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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /app
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/dev_ops_report_no_data.svg (renamed from app/views/shared/icons/_dev_ops_report_no_data.svg)0
-rw-r--r--app/assets/images/learn_gitlab/get_started.svg1
-rw-r--r--app/assets/images/learn_gitlab/graduation_hat.svg1
-rw-r--r--app/assets/images/learn_gitlab/rectangle.svg1
-rw-r--r--app/assets/javascripts/actioncable_link.js40
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue12
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue12
-rw-r--r--app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue5
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unblock.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/usage_ping_disabled.vue48
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue5
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue66
-rw-r--r--app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql8
-rw-r--r--app/assets/javascripts/admin/users/index.js30
-rw-r--r--app/assets/javascripts/admin/users/tabs.js32
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue4
-rw-r--r--app/assets/javascripts/alert_management/list.js2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue25
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue84
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql (renamed from app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql)5
-rw-r--r--app/assets/javascripts/alerts_settings/utils/cache_updates.js33
-rw-r--r--app/assets/javascripts/alerts_settings/utils/mapping_transformations.js22
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score.vue110
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score.js22
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js)2
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue11
-rw-r--r--app/assets/javascripts/awards_handler.js1
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue5
-rw-r--r--app/assets/javascripts/batch_comments/components/drafts_count.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_dropdown.vue16
-rw-r--r--app/assets/javascripts/batch_comments/components/publish_button.vue2
-rw-r--r--app/assets/javascripts/behaviors/date_picker.js33
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js19
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcut.vue80
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue574
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue6
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue1
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js2
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js29
-rw-r--r--app/assets/javascripts/blob/viewer/index.js18
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js9
-rw-r--r--app/assets/javascripts/boards/boards_util.js31
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue44
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue154
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue46
-rw-r--r--app/assets/javascripts/boards/constants.js42
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js4
-rw-r--r--app/assets/javascripts/boards/graphql/group_projects.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/boards/index.js24
-rw-r--r--app/assets/javascripts/boards/stores/actions.js113
-rw-r--r--app/assets/javascripts/boards/stores/getters.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js10
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js48
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue23
-rw-r--r--app/assets/javascripts/code_quality_walkthrough/components/step.vue150
-rw-r--r--app/assets/javascripts/code_quality_walkthrough/constants.js67
-rw-r--r--app/assets/javascripts/code_quality_walkthrough/index.js14
-rw-r--r--app/assets/javascripts/code_quality_walkthrough/utils.js38
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue22
-rw-r--r--app/assets/javascripts/content_editor/components/divider.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue65
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue94
-rw-r--r--app/assets/javascripts/content_editor/constants.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/bold.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/bullet_list.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/code.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js53
-rw-r--r--app/assets/javascripts/content_editor/extensions/document.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/dropcursor.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/gapcursor.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/hard_break.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/heading.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/history.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/horizontal_rule.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/italic.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/list_item.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/ordered_list.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/paragraph.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/text.js5
-rw-r--r--app/assets/javascripts/content_editor/index.js2
-rw-r--r--app/assets/javascripts/content_editor/services/build_serializer_config.js22
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js25
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js76
-rw-r--r--app/assets/javascripts/content_editor/services/create_editor.js60
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js101
-rw-r--r--app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js61
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue134
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js35
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js112
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js18
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js51
-rw-r--r--app/assets/javascripts/cycle_analytics/store/index.js21
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js52
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js63
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js2
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue29
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue43
-rw-r--r--app/assets/javascripts/deploy_keys/components/confirm_modal.vue46
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue52
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue16
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue96
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/compare_dropdown_layout.vue65
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue27
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue13
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue51
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue1
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue4
-rw-r--r--app/assets/javascripts/diffs/constants.js17
-rw-r--r--app/assets/javascripts/diffs/store/actions.js53
-rw-r--r--app/assets/javascripts/diffs/store/getters.js3
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js20
-rw-r--r--app/assets/javascripts/diffs/utils/diff_file.js2
-rw-r--r--app/assets/javascripts/due_date_select.js33
-rw-r--r--app/assets/javascripts/editor/editor_lite.js2
-rw-r--r--app/assets/javascripts/editor/extensions/editor_lite_extension_base.js8
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue1
-rw-r--r--app/assets/javascripts/ensure_data.js4
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue40
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js2
-rw-r--r--app/assets/javascripts/environments/stores/helpers.js2
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue32
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue2
-rw-r--r--app/assets/javascripts/experimentation/components/gitlab_experiment.vue (renamed from app/assets/javascripts/experimentation/components/experiment.vue)0
-rw-r--r--app/assets/javascripts/experimentation/utils.js11
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue5
-rw-r--r--app/assets/javascripts/feature_flags/index.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js2
-rw-r--r--app/assets/javascripts/flash.js12
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue29
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue6
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue5
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue7
-rw-r--r--app/assets/javascripts/frequent_items/constants.js14
-rw-r--r--app/assets/javascripts/frequent_items/index.js46
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js29
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql10
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql9
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue26
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/help/help.js11
-rw-r--r--app/assets/javascripts/ide/components/branches/item.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue1
-rw-r--r--app/assets/javascripts/ide/components/file_alert.vue26
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue37
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/lib/alerts/environments.vue32
-rw-r--r--app/assets/javascripts/ide/lib/alerts/index.js20
-rw-r--r--app/assets/javascripts/ide/messages.js6
-rw-r--r--app/assets/javascripts/ide/services/gql.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js16
-rw-r--r--app/assets/javascripts/ide/stores/actions.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/alert.js18
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/getters/alert.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations/alert.js21
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue80
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js244
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql15
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql9
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js111
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js14
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql65
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue34
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js4
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_checkbox.vue1
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue6
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_modal.vue67
-rw-r--r--app/assets/javascripts/invite_member/components/invite_member_trigger.vue43
-rw-r--r--app/assets/javascripts/invite_member/constants.js2
-rw-r--r--app/assets/javascripts/invite_member/event_hub.js3
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_modal.js27
-rw-r--r--app/assets/javascripts/invite_member/init_invite_member_trigger.js18
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue6
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue103
-rw-r--r--app/assets/javascripts/issuable_form.js16
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue17
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue77
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue5
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue64
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue622
-rw-r--r--app/assets/javascripts/issues_list/constants.js366
-rw-r--r--app/assets/javascripts/issues_list/index.js42
-rw-r--r--app/assets/javascripts/issues_list/utils.js195
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue32
-rw-r--r--app/assets/javascripts/jira_connect/constants.js3
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue26
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue27
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue7
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/actions_cell.vue14
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/duration_cell.vue49
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/job_cell.vue163
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue50
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql17
-rw-r--r--app/assets/javascripts/jobs/components/table/index.js10
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue85
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue14
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue35
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue2
-rw-r--r--app/assets/javascripts/jobs/constants.js2
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/learn_gitlab/track_learn_gitlab.js10
-rw-r--r--app/assets/javascripts/lib/graphql.js47
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js3
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/keys.js1
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/recurrence.js154
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js2
-rw-r--r--app/assets/javascripts/lib/utils/uuids.js (renamed from app/assets/javascripts/diffs/utils/uuids.js)0
-rw-r--r--app/assets/javascripts/lib/utils/vuex_module_mappers.js91
-rw-r--r--app/assets/javascripts/logs/components/log_advanced_filters.vue5
-rw-r--r--app/assets/javascripts/logs/stores/actions.js2
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue16
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue124
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue32
-rw-r--r--app/assets/javascripts/members/index.js2
-rw-r--r--app/assets/javascripts/members/store/state.js2
-rw-r--r--app/assets/javascripts/members/utils.js18
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue34
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue111
-rw-r--r--app/assets/javascripts/merge_request/components/status_box.vue71
-rw-r--r--app/assets/javascripts/merge_request/eventhub.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue4
-rw-r--r--app/assets/javascripts/monitoring/monitoring_app.js6
-rw-r--r--app/assets/javascripts/monitoring/utils.js1
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue59
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue74
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue144
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue31
-rw-r--r--app/assets/javascripts/nav/index.js12
-rw-r--r--app/assets/javascripts/nav/mount.js23
-rw-r--r--app/assets/javascripts/nav/stores/index.js4
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue35
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue26
-rw-r--r--app/assets/javascripts/notes/stores/actions.js13
-rw-r--r--app/assets/javascripts/notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/packages/details/components/maven_installation.vue30
-rw-r--r--app/assets/javascripts/packages/details/components/npm_installation.vue27
-rw-r--r--app/assets/javascripts/packages/details/components/package_files.vue1
-rw-r--r--app/assets/javascripts/packages/details/constants.js3
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js11
-rw-r--r--app/assets/javascripts/packages/list/components/package_search.vue5
-rw-r--r--app/assets/javascripts/packages/list/constants.js2
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js6
-rw-r--r--app/assets/javascripts/packages/list/stores/mutations.js3
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue35
-rw-r--r--app/assets/javascripts/packages/shared/components/package_path.vue19
-rw-r--r--app/assets/javascripts/packages/shared/constants.js5
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue118
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue38
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue114
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue25
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js12
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue (renamed from app/assets/javascripts/registry/settings/components/expiration_dropdown.vue)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue (renamed from app/assets/javascripts/registry/settings/components/expiration_input.vue)5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue (renamed from app/assets/javascripts/registry/settings/components/expiration_run_text.vue)5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue (renamed from app/assets/javascripts/registry/settings/components/expiration_toggle.vue)5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue (renamed from app/assets/javascripts/registry/settings/components/registry_settings_app.vue)4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue (renamed from app/assets/javascripts/registry/settings/components/settings_form.vue)8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js (renamed from app/assets/javascripts/registry/settings/constants.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql (renamed from app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js (renamed from app/assets/javascripts/registry/settings/graphql/index.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql (renamed from app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql (renamed from app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.query.graphql)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js (renamed from app/assets/javascripts/registry/settings/graphql/utils/cache_update.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js (renamed from app/assets/javascripts/registry/settings/registry_settings_bundle.js)0
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/utils.js (renamed from app/assets/javascripts/registry/settings/utils.js)0
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/labels/index/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue18
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/milestones/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/milestones/new/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js (renamed from app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js)0
-rw-r--r--app/assets/javascripts/pages/groups/settings/repository/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js2
-rw-r--r--app/assets/javascripts/pages/help/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue126
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js42
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js4
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue17
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue10
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue12
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue10
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js (renamed from app/assets/javascripts/compare_autocomplete.js)8
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js16
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql7
-rw-r--r--app/assets/javascripts/pages/projects/milestones/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/new/components/app.vue148
-rw-r--r--app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue (renamed from app/assets/javascripts/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue)0
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js54
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js18
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js4
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js8
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue298
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/performance/constants.js12
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue67
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue35
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue75
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue24
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue105
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue17
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue43
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue154
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue12
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue44
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue155
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js6
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql1
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql13
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js17
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js27
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue146
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue10
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue26
-rw-r--r--app/assets/javascripts/pipeline_new/components/refs_dropdown.vue3
-rw-r--r--app/assets/javascripts/pipeline_new/constants.js3
-rw-r--r--app/assets/javascripts/pipeline_new/utils/filter_variables.js13
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue63
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue159
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js7
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/api.js5
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js1
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue89
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue61
-rw-r--r--app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js25
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue76
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue115
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue21
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue89
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/empty_state.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue10
-rw-r--r--app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js6
-rw-r--r--app/assets/javascripts/pipelines/utils.js9
-rw-r--r--app/assets/javascripts/project_select.js1
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue57
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue52
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue26
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue15
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue2
-rw-r--r--app/assets/javascripts/projects/compare/index.js6
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue201
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue66
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/constants.js1
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg9
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg23
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg13
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg38
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/index.js20
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue11
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue60
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue155
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue30
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql27
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql29
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql6
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue105
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue2
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_graphql.vue35
-rw-r--r--app/assets/javascripts/releases/components/releases_pagination_rest.vue24
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue17
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql (renamed from app/assets/javascripts/releases/queries/release.fragment.graphql)0
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql23
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql10
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql5
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql (renamed from app/assets/javascripts/releases/queries/all_releases.query.graphql)2
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release.query.graphql (renamed from app/assets/javascripts/releases/queries/one_release.query.graphql)2
-rw-r--r--app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql9
-rw-r--r--app/assets/javascripts/releases/mount_index.js5
-rw-r--r--app/assets/javascripts/releases/stores/getters.js11
-rw-r--r--app/assets/javascripts/releases/stores/index.js2
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js252
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js36
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/actions.js66
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/mutations.js8
-rw-r--r--app/assets/javascripts/releases/stores/modules/index/state.js3
-rw-r--r--app/assets/javascripts/releases/util.js87
-rw-r--r--app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue5
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue18
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js31
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js3
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js (renamed from app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js)16
-rw-r--r--app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js28
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue10
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue111
-rw-r--r--app/assets/javascripts/repository/components/blob_header_edit.vue25
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue2
-rw-r--r--app/assets/javascripts/repository/index.js5
-rw-r--r--app/assets/javascripts/repository/pages/blob.vue6
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql48
-rw-r--r--app/assets/javascripts/repository/router.js1
-rw-r--r--app/assets/javascripts/runner/components/runner_type_badge.vue45
-rw-r--r--app/assets/javascripts/runner/constants.js11
-rw-r--r--app/assets/javascripts/runner/graphql/get_runner.query.graphql6
-rw-r--r--app/assets/javascripts/runner/runner_details/constants.js3
-rw-r--r--app/assets/javascripts/runner/runner_details/index.js16
-rw-r--r--app/assets/javascripts/runner/runner_details/runner_details_app.vue29
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue15
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js (renamed from app/assets/javascripts/security_configuration/components/scanners_constants.js)16
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue150
-rw-r--r--app/assets/javascripts/security_configuration/components/manage_sast.vue59
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade.vue2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue7
-rw-r--r--app/assets/javascripts/shared/milestones/form.js5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue79
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue287
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue44
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue296
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue56
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue110
-rw-r--r--app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue203
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue68
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue42
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue202
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue112
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue24
-rw-r--r--app/assets/javascripts/sidebar/constants.js98
-rw-r--r--app/assets/javascripts/sidebar/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/sidebar/graphql.js16
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js47
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql13
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_participants.query.graphql18
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql13
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql16
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql10
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql11
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql11
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js1
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js2
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue1
-rw-r--r--app/assets/javascripts/static_site_editor/services/generate_branch_name.js4
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js26
-rw-r--r--app/assets/javascripts/task_list.js21
-rw-r--r--app/assets/javascripts/tracking.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js23
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue3
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue46
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue56
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue7
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue (renamed from app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue)2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue (renamed from app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js48
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js40
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue167
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/keep_alive_slots.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/remove_member_modal.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql16
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql6
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue302
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue21
-rw-r--r--app/assets/javascripts/vue_shared/directives/validation.js66
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js20
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue (renamed from app/assets/javascripts/projects/experiment_new_project_creation/components/legacy_container.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue71
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue135
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue83
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/provider.js9
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql (renamed from app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql18
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue8
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/utils.js14
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue13
-rw-r--r--app/assets/javascripts/whats_new/components/feature.vue60
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js4
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss4
-rw-r--r--app/assets/stylesheets/components/feature_highlight.scss22
-rw-r--r--app/assets/stylesheets/components/whats_new.scss12
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/blank.scss136
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss9
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss24
-rw-r--r--app/assets/stylesheets/framework/diffs.scss6
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss102
-rw-r--r--app/assets/stylesheets/framework/editor-lite.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss60
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss11
-rw-r--r--app/assets/stylesheets/framework/kbd.scss15
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss5
-rw-r--r--app/assets/stylesheets/framework/spinner.scss49
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss16
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss25
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/dev_ops_report.scss261
-rw-r--r--app/assets/stylesheets/page_bundles/jira_connect.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/merge_conflicts.scss17
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss17
-rw-r--r--app/assets/stylesheets/page_bundles/new_namespace.scss28
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/todos.scss170
-rw-r--r--app/assets/stylesheets/pages/editor.scss27
-rw-r--r--app/assets/stylesheets/pages/help.scss44
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/assets/stylesheets/pages/projects.scss10
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss4
-rw-r--r--app/assets/stylesheets/themes/_dark.scss6
-rw-r--r--app/assets/stylesheets/utilities.scss35
-rw-r--r--app/channels/graphql_channel.rb56
-rw-r--r--app/channels/issues_channel.rb13
-rw-r--r--app/controllers/admin/application_controller.rb2
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/cohorts_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/admin/dev_ops_report_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/health_check_controller.rb2
-rw-r--r--app/controllers/admin/integrations_controller.rb2
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/plan_limits_controller.rb1
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb4
-rw-r--r--app/controllers/admin/services_controller.rb20
-rw-r--r--app/controllers/admin/users_controller.rb53
-rw-r--r--app/controllers/application_controller.rb11
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb6
-rw-r--r--app/controllers/boards/lists_controller.rb6
-rw-r--r--app/controllers/clusters/clusters_controller.rb3
-rw-r--r--app/controllers/clusters/integrations_controller.rb2
-rw-r--r--app/controllers/concerns/accepts_pending_invitations.rb10
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb2
-rw-r--r--app/controllers/concerns/boards_actions.rb12
-rw-r--r--app/controllers/concerns/boards_responses.rb2
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb2
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb2
-rw-r--r--app/controllers/concerns/floc_opt_out.rb17
-rw-r--r--app/controllers/concerns/integrations/params.rb105
-rw-r--r--app/controllers/concerns/integrations_actions.rb8
-rw-r--r--app/controllers/concerns/internal_redirect.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb7
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb4
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb2
-rw-r--r--app/controllers/concerns/page_limiter.rb2
-rw-r--r--app/controllers/concerns/renders_commits.rb1
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb2
-rw-r--r--app/controllers/concerns/routable_actions.rb2
-rw-r--r--app/controllers/concerns/service_params.rb101
-rw-r--r--app/controllers/concerns/wiki_actions.rb7
-rw-r--r--app/controllers/concerns/with_performance_bar.rb12
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard_controller.rb3
-rw-r--r--app/controllers/explore/projects_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/autocomplete_sources_controller.rb54
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/email_campaigns_controller.rb6
-rw-r--r--app/controllers/groups/group_members_controller.rb24
-rw-r--r--app/controllers/groups/milestones_controller.rb6
-rw-r--r--app/controllers/groups/runners_controller.rb1
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb6
-rw-r--r--app/controllers/groups/settings/packages_and_registries_controller.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb11
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb2
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb59
-rw-r--r--app/controllers/jira_connect/application_controller.rb2
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/oauth/jira/authorizations_controller.rb10
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/accounts_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb2
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/stages_controller.rb44
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb16
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/blame_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb24
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb4
-rw-r--r--app/controllers/projects/commit_controller.rb6
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb2
-rw-r--r--app/controllers/projects/group_links_controller.rb2
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/imports_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb35
-rw-r--r--app/controllers/projects/logs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb24
-rw-r--r--app/controllers/projects/milestones_controller.rb4
-rw-r--r--app/controllers/projects/mirrors_controller.rb2
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb39
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/protected_branches_controller.rb2
-rw-r--r--app/controllers/projects/protected_refs_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb5
-rw-r--r--app/controllers/projects/repositories_controller.rb4
-rw-r--r--app/controllers/projects/runner_projects_controller.rb5
-rw-r--r--app/controllers/projects/runners_controller.rb15
-rw-r--r--app/controllers/projects/security/configuration_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb48
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/controllers/projects/settings/operations_controller.rb2
-rw-r--r--app/controllers/projects/settings/packages_and_registries_controller.rb23
-rw-r--r--app/controllers/projects/settings/repository_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb4
-rw-r--r--app/controllers/projects/tags_controller.rb3
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb9
-rw-r--r--app/controllers/registrations/experience_levels_controller.rb2
-rw-r--r--app/controllers/registrations/invites_controller.rb9
-rw-r--r--app/controllers/registrations/welcome_controller.rb19
-rw-r--r--app/controllers/registrations_controller.rb27
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb2
-rw-r--r--app/controllers/repositories/git_http_controller.rb2
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb2
-rw-r--r--app/controllers/root_controller.rb2
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb10
-rw-r--r--app/controllers/terraform/services_controller.rb11
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/controllers/whats_new_controller.rb5
-rw-r--r--app/experiments/application_experiment.rb4
-rw-r--r--app/experiments/concerns/project_commit_count.rb21
-rw-r--r--app/experiments/empty_repo_upload_experiment.rb22
-rw-r--r--app/experiments/in_product_guidance_environments_webide_experiment.rb15
-rw-r--r--app/experiments/members/invite_email_experiment.rb4
-rw-r--r--app/experiments/new_project_readme_experiment.rb25
-rw-r--r--app/finders/alert_management/alerts_finder.rb2
-rw-r--r--app/finders/alert_management/http_integrations_finder.rb2
-rw-r--r--app/finders/analytics/cycle_analytics/stage_finder.rb37
-rw-r--r--app/finders/autocomplete/users_finder.rb2
-rw-r--r--app/finders/ci/daily_build_group_report_results_finder.rb2
-rw-r--r--app/finders/ci/pipelines_finder.rb2
-rw-r--r--app/finders/ci/pipelines_for_merge_request_finder.rb7
-rw-r--r--app/finders/ci/runners_finder.rb7
-rw-r--r--app/finders/concerns/packages/finder_helper.rb16
-rw-r--r--app/finders/deploy_tokens/tokens_finder.rb43
-rw-r--r--app/finders/deployments_finder.rb105
-rw-r--r--app/finders/environment_names_finder.rb57
-rw-r--r--app/finders/environments/environment_names_finder.rb59
-rw-r--r--app/finders/environments/environments_by_deployments_finder.rb69
-rw-r--r--app/finders/environments/environments_finder.rb65
-rw-r--r--app/finders/environments_by_deployments_finder.rb67
-rw-r--r--app/finders/environments_finder.rb63
-rw-r--r--app/finders/fork_targets_finder.rb2
-rw-r--r--app/finders/group_members_finder.rb8
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb14
-rw-r--r--app/finders/issuables/author_filter.rb2
-rw-r--r--app/finders/issuables/base_filter.rb7
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/issues_finder/params.rb2
-rw-r--r--app/finders/license_template_finder.rb2
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/finders/namespaces/projects_finder.rb2
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/finders/packages/composer/packages_finder.rb2
-rw-r--r--app/finders/packages/conan/package_finder.rb2
-rw-r--r--app/finders/packages/generic/package_finder.rb1
-rw-r--r--app/finders/packages/go/package_finder.rb1
-rw-r--r--app/finders/packages/go/version_finder.rb2
-rw-r--r--app/finders/packages/group_or_project_package_finder.rb45
-rw-r--r--app/finders/packages/group_packages_finder.rb6
-rw-r--r--app/finders/packages/maven/package_finder.rb72
-rw-r--r--app/finders/packages/npm/package_finder.rb1
-rw-r--r--app/finders/packages/nuget/package_finder.rb37
-rw-r--r--app/finders/packages/package_finder.rb2
-rw-r--r--app/finders/packages/packages_finder.rb1
-rw-r--r--app/finders/packages/pypi/package_finder.rb17
-rw-r--r--app/finders/packages/pypi/packages_finder.rb20
-rw-r--r--app/finders/projects/groups_finder.rb35
-rw-r--r--app/finders/projects/members/effective_access_level_finder.rb125
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/repositories/branch_names_finder.rb12
-rw-r--r--app/finders/snippets_finder.rb2
-rw-r--r--app/finders/template_finder.rb3
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/finders/users_finder.rb2
-rw-r--r--app/finders/users_with_pending_todos_finder.rb16
-rw-r--r--app/graphql/gitlab_schema.rb4
-rw-r--r--app/graphql/graphql_triggers.rb7
-rw-r--r--app/graphql/mutations/alert_management/http_integration/create.rb2
-rw-r--r--app/graphql/mutations/alert_management/http_integration/http_integration_base.rb2
-rw-r--r--app/graphql/mutations/alert_management/http_integration/update.rb2
-rw-r--r--app/graphql/mutations/boards/create.rb2
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb2
-rw-r--r--app/graphql/mutations/boards/lists/base_update.rb40
-rw-r--r--app/graphql/mutations/boards/lists/create.rb2
-rw-r--r--app/graphql/mutations/boards/lists/update.rb22
-rw-r--r--app/graphql/mutations/boards/update.rb2
-rw-r--r--app/graphql/mutations/ci/ci_cd_settings_update.rb2
-rw-r--r--app/graphql/mutations/ci/job/base.rb22
-rw-r--r--app/graphql/mutations/ci/job/play.rb29
-rw-r--r--app/graphql/mutations/ci/job/retry.rb29
-rw-r--r--app/graphql/mutations/commits/create.rb11
-rw-r--r--app/graphql/mutations/concerns/mutations/assignable.rb6
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_issuable.rb2
-rw-r--r--app/graphql/mutations/issues/common_mutation_arguments.rb5
-rw-r--r--app/graphql/mutations/issues/create.rb4
-rw-r--r--app/graphql/mutations/issues/move.rb2
-rw-r--r--app/graphql/mutations/issues/set_confidential.rb2
-rw-r--r--app/graphql/mutations/issues/set_due_date.rb15
-rw-r--r--app/graphql/mutations/issues/set_locked.rb2
-rw-r--r--app/graphql/mutations/issues/set_severity.rb2
-rw-r--r--app/graphql/mutations/issues/update.rb4
-rw-r--r--app/graphql/mutations/labels/create.rb13
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb2
-rw-r--r--app/graphql/mutations/merge_requests/create.rb2
-rw-r--r--app/graphql/mutations/merge_requests/reviewer_rereview.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_draft.rb35
-rw-r--r--app/graphql/mutations/merge_requests/set_labels.rb6
-rw-r--r--app/graphql/mutations/merge_requests/set_locked.rb4
-rw-r--r--app/graphql/mutations/merge_requests/set_milestone.rb4
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb2
-rw-r--r--app/graphql/mutations/merge_requests/update.rb2
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb10
-rw-r--r--app/graphql/mutations/security/ci_configuration/configure_sast.rb17
-rw-r--r--app/graphql/mutations/security/ci_configuration/configure_secret_detection.rb48
-rw-r--r--app/graphql/queries/burndown_chart/burnup.query.graphql70
-rw-r--r--app/graphql/queries/epic/epic_children.query.graphql4
-rw-r--r--app/graphql/queries/pipelines/get_pipeline_details.query.graphql1
-rw-r--r--app/graphql/resolvers/ci/runner_resolver.rb36
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb43
-rw-r--r--app/graphql/resolvers/ci/template_resolver.rb18
-rw-r--r--app/graphql/resolvers/concerns/board_issue_filterable.rb2
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb70
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb1
-rw-r--r--app/graphql/resolvers/environments_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_packages_resolver.rb26
-rw-r--r--app/graphql/resolvers/issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb2
-rw-r--r--app/graphql/resolvers/packages_base_resolver.rb53
-rw-r--r--app/graphql/resolvers/project_packages_resolver.rb16
-rw-r--r--app/graphql/resolvers/projects/services_resolver.rb2
-rw-r--r--app/graphql/resolvers/release_resolver.rb2
-rw-r--r--app/graphql/resolvers/releases_resolver.rb2
-rw-r--r--app/graphql/resolvers/repository_branch_names_resolver.rb12
-rw-r--r--app/graphql/subscriptions/base_subscription.rb31
-rw-r--r--app/graphql/subscriptions/issuable_updated.rb29
-rw-r--r--app/graphql/types/access_level_enum.rb14
-rw-r--r--app/graphql/types/alert_management/http_integration_type.rb2
-rw-r--r--app/graphql/types/alert_management/status_enum.rb2
-rw-r--r--app/graphql/types/base_argument.rb3
-rw-r--r--app/graphql/types/base_enum.rb4
-rw-r--r--app/graphql/types/base_field.rb3
-rw-r--r--app/graphql/types/blob_viewer_type.rb47
-rw-r--r--app/graphql/types/board_list_type.rb2
-rw-r--r--app/graphql/types/board_type.rb2
-rw-r--r--app/graphql/types/boards/board_issue_input_base_type.rb6
-rw-r--r--app/graphql/types/boards/board_issue_input_type.rb7
-rw-r--r--app/graphql/types/boards/negated_board_issue_input_type.rb2
-rw-r--r--app/graphql/types/ci/code_quality_degradation_severity_enum.rb15
-rw-r--r--app/graphql/types/ci/job_type.rb30
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb2
-rw-r--r--app/graphql/types/ci/pipeline_type.rb24
-rw-r--r--app/graphql/types/ci/runner_access_level_enum.rb15
-rw-r--r--app/graphql/types/ci/runner_sort_enum.rb13
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb15
-rw-r--r--app/graphql/types/ci/runner_type.rb42
-rw-r--r--app/graphql/types/ci/runner_type_enum.rb15
-rw-r--r--app/graphql/types/ci/stage_type.rb55
-rw-r--r--app/graphql/types/ci/template_type.rb16
-rw-r--r--app/graphql/types/container_expiration_policy_cadence_enum.rb2
-rw-r--r--app/graphql/types/container_expiration_policy_keep_enum.rb2
-rw-r--r--app/graphql/types/container_expiration_policy_older_than_enum.rb2
-rw-r--r--app/graphql/types/design_management/version_type.rb4
-rw-r--r--app/graphql/types/duration_type.rb29
-rw-r--r--app/graphql/types/group_member_relation_enum.rb2
-rw-r--r--app/graphql/types/group_type.rb4
-rw-r--r--app/graphql/types/issuable_type.rb23
-rw-r--r--app/graphql/types/issue_connection_type.rb2
-rw-r--r--app/graphql/types/issue_sort_enum.rb2
-rw-r--r--app/graphql/types/issue_type.rb2
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb2
-rw-r--r--app/graphql/types/label_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb10
-rw-r--r--app/graphql/types/merge_requests/assignee_type.rb14
-rw-r--r--app/graphql/types/merge_requests/interacts_with_merge_request.rb24
-rw-r--r--app/graphql/types/merge_requests/reviewer_type.rb16
-rw-r--r--app/graphql/types/metadata/kas_type.rb18
-rw-r--r--app/graphql/types/metadata_type.rb2
-rw-r--r--app/graphql/types/milestone_type.rb4
-rw-r--r--app/graphql/types/mutation_type.rb10
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb2
-rw-r--r--app/graphql/types/namespace_type.rb2
-rw-r--r--app/graphql/types/notes/noteable_type.rb2
-rw-r--r--app/graphql/types/notes/position_type_enum.rb4
-rw-r--r--app/graphql/types/packages/maven/metadatum_type.rb22
-rw-r--r--app/graphql/types/packages/metadata_type.rb6
-rw-r--r--app/graphql/types/packages/nuget/metadatum_type.rb19
-rw-r--r--app/graphql/types/packages/package_group_sort_enum.rb15
-rw-r--r--app/graphql/types/packages/package_sort_enum.rb19
-rw-r--r--app/graphql/types/packages/package_status_enum.rb13
-rw-r--r--app/graphql/types/packages/package_type.rb5
-rw-r--r--app/graphql/types/packages/package_type_enum.rb5
-rw-r--r--app/graphql/types/permission_types/ci/job.rb14
-rw-r--r--app/graphql/types/permission_types/project.rb2
-rw-r--r--app/graphql/types/project_type.rb23
-rw-r--r--app/graphql/types/projects/service_type_enum.rb2
-rw-r--r--app/graphql/types/query_type.rb32
-rw-r--r--app/graphql/types/release_assets_type.rb2
-rw-r--r--app/graphql/types/repository/blob_type.rb60
-rw-r--r--app/graphql/types/repository_type.rb4
-rw-r--r--app/graphql/types/snippets/blob_viewer_type.rb44
-rw-r--r--app/graphql/types/snippets/type_enum.rb4
-rw-r--r--app/graphql/types/subscription_type.rb10
-rw-r--r--app/graphql/types/timelog_type.rb9
-rw-r--r--app/graphql/types/todo_target_enum.rb2
-rw-r--r--app/graphql/types/tree/type_enum.rb6
-rw-r--r--app/graphql/types/user_interface.rb111
-rw-r--r--app/graphql/types/user_merge_request_interaction_type.rb2
-rw-r--r--app/graphql/types/user_type.rb97
-rw-r--r--app/helpers/analytics/navbar_helper.rb43
-rw-r--r--app/helpers/appearances_helper.rb3
-rw-r--r--app/helpers/application_helper.rb34
-rw-r--r--app/helpers/application_settings_helper.rb17
-rw-r--r--app/helpers/auth_helper.rb31
-rw-r--r--app/helpers/avatars_helper.rb13
-rw-r--r--app/helpers/award_emoji_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/boards_helper.rb8
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/ci/jobs_helper.rb5
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb12
-rw-r--r--app/helpers/ci/pipelines_helper.rb3
-rw-r--r--app/helpers/ci/runners_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb23
-rw-r--r--app/helpers/dashboard_helper.rb2
-rw-r--r--app/helpers/dev_ops_report_helper.rb74
-rw-r--r--app/helpers/diff_helper.rb6
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb11
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/export_helper.rb2
-rw-r--r--app/helpers/feature_flags_helper.rb2
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/graph_helper.rb2
-rw-r--r--app/helpers/groups/group_members_helper.rb34
-rw-r--r--app/helpers/groups_helper.rb48
-rw-r--r--app/helpers/hooks_helper.rb2
-rw-r--r--app/helpers/ide_helper.rb17
-rw-r--r--app/helpers/in_product_marketing_helper.rb379
-rw-r--r--app/helpers/invite_members_helper.rb33
-rw-r--r--app/helpers/issuables_helper.rb20
-rw-r--r--app/helpers/issues_helper.rb29
-rw-r--r--app/helpers/kerberos_spnego_helper.rb2
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/helpers/learn_gitlab_helper.rb66
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/members_helper.rb10
-rw-r--r--app/helpers/merge_requests_helper.rb12
-rw-r--r--app/helpers/mirror_helper.rb2
-rw-r--r--app/helpers/namespace_storage_limit_alert_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb11
-rw-r--r--app/helpers/nav/top_nav_helper.rb243
-rw-r--r--app/helpers/nav_helper.rb12
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/helpers/notify_helper.rb2
-rw-r--r--app/helpers/operations_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb20
-rw-r--r--app/helpers/profiles_helper.rb2
-rw-r--r--app/helpers/projects/alert_management_helper.rb5
-rw-r--r--app/helpers/projects/incidents_helper.rb2
-rw-r--r--app/helpers/projects/project_members_helper.rb32
-rw-r--r--app/helpers/projects/security/configuration_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb182
-rw-r--r--app/helpers/registrations_helper.rb12
-rw-r--r--app/helpers/releases_helper.rb2
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/selects_helper.rb2
-rw-r--r--app/helpers/services_helper.rb12
-rw-r--r--app/helpers/sidebars_helper.rb8
-rw-r--r--app/helpers/snippets_helper.rb6
-rw-r--r--app/helpers/sorting_helper.rb2
-rw-r--r--app/helpers/sorting_titles_values_helper.rb2
-rw-r--r--app/helpers/ssh_keys_helper.rb5
-rw-r--r--app/helpers/subscribable_banner_helper.rb2
-rw-r--r--app/helpers/system_note_helper.rb4
-rw-r--r--app/helpers/time_zone_helper.rb2
-rw-r--r--app/helpers/timeboxes_helper.rb2
-rw-r--r--app/helpers/timeboxes_routing_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb8
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb4
-rw-r--r--app/helpers/users_helper.rb50
-rw-r--r--app/helpers/version_check_helper.rb14
-rw-r--r--app/helpers/webpack_helper.rb22
-rw-r--r--app/helpers/whats_new_helper.rb29
-rw-r--r--app/helpers/wiki_helper.rb2
-rw-r--r--app/helpers/x509_helper.rb2
-rw-r--r--app/mailers/emails/in_product_marketing.rb25
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/members.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/emails/profile.rb2
-rw-r--r--app/mailers/emails/projects.rb2
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/mailers/previews/notify_preview.rb8
-rw-r--r--app/models/ability.rb2
-rw-r--r--app/models/alert_management/alert.rb10
-rw-r--r--app/models/alert_management/http_integration.rb2
-rw-r--r--app/models/alerting/project_alerting_setting.rb2
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb3
-rw-r--r--app/models/analytics/cycle_analytics/project_value_stream.rb22
-rw-r--r--app/models/analytics/usage_trends/measurement.rb2
-rw-r--r--app/models/application_record.rb8
-rw-r--r--app/models/application_setting.rb70
-rw-r--r--app/models/application_setting_implementation.rb12
-rw-r--r--app/models/atlassian/identity.rb4
-rw-r--r--app/models/audit_event.rb2
-rw-r--r--app/models/blob_viewer/dependency_manager.rb2
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/board_group_recent_visit.rb20
-rw-r--r--app/models/board_project_recent_visit.rb20
-rw-r--r--app/models/broadcast_message.rb10
-rw-r--r--app/models/bulk_imports/configuration.rb4
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/bulk_imports/export.rb61
-rw-r--r--app/models/bulk_imports/export_upload.rb18
-rw-r--r--app/models/bulk_imports/file_transfer.rb20
-rw-r--r--app/models/bulk_imports/file_transfer/base_config.rb59
-rw-r--r--app/models/bulk_imports/file_transfer/group_config.rb15
-rw-r--r--app/models/bulk_imports/file_transfer/project_config.rb15
-rw-r--r--app/models/bulk_imports/stage.rb65
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/chat_name.rb4
-rw-r--r--app/models/ci/bridge.rb5
-rw-r--r--app/models/ci/build.rb19
-rw-r--r--app/models/ci/build_dependencies.rb51
-rw-r--r--app/models/ci/build_need.rb10
-rw-r--r--app/models/ci/build_runner_session.rb3
-rw-r--r--app/models/ci/build_trace_chunk.rb3
-rw-r--r--app/models/ci/commit_with_pipeline.rb18
-rw-r--r--app/models/ci/daily_build_group_report_result.rb2
-rw-r--r--app/models/ci/deleted_object.rb2
-rw-r--r--app/models/ci/job_artifact.rb18
-rw-r--r--app/models/ci/persistent_ref.rb6
-rw-r--r--app/models/ci/pipeline.rb34
-rw-r--r--app/models/ci/pipeline_artifact.rb4
-rw-r--r--app/models/ci/pipeline_schedule.rb30
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/ci/runner_namespace.rb5
-rw-r--r--app/models/ci/runner_project.rb5
-rw-r--r--app/models/ci/stage.rb2
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/unit_test.rb1
-rw-r--r--app/models/ci/unit_test_failure.rb2
-rw-r--r--app/models/clusters/agent.rb1
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/clusters/applications/elastic_stack.rb48
-rw-r--r--app/models/clusters/applications/prometheus.rb33
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb24
-rw-r--r--app/models/clusters/concerns/application_core.rb22
-rw-r--r--app/models/clusters/concerns/elasticsearch_client.rb38
-rw-r--r--app/models/clusters/concerns/kubernetes_logger.rb27
-rw-r--r--app/models/clusters/integrations/elastic_stack.rb38
-rw-r--r--app/models/clusters/integrations/prometheus.rb38
-rw-r--r--app/models/clusters/providers/aws.rb2
-rw-r--r--app/models/commit.rb1
-rw-r--r--app/models/commit_status.rb10
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb1
-rw-r--r--app/models/concerns/atomic_internal_id.rb4
-rw-r--r--app/models/concerns/board_recent_visit.rb34
-rw-r--r--app/models/concerns/cache_markdown_field.rb4
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb26
-rw-r--r--app/models/concerns/ci/artifactable.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb10
-rw-r--r--app/models/concerns/ci/maskable.rb4
-rw-r--r--app/models/concerns/ci/metadatable.rb2
-rw-r--r--app/models/concerns/cron_schedulable.rb41
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb4
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb1
-rw-r--r--app/models/concerns/enums/internal_id.rb2
-rw-r--r--app/models/concerns/enums/vulnerability.rb2
-rw-r--r--app/models/concerns/from_set_operator.rb4
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/has_integrations.rb (renamed from app/models/concerns/integration.rb)8
-rw-r--r--app/models/concerns/has_repository.rb4
-rw-r--r--app/models/concerns/has_timelogs_report.rb2
-rw-r--r--app/models/concerns/has_wiki_page_meta_attributes.rb2
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/concerns/issue_available_features.rb4
-rw-r--r--app/models/concerns/limitable.rb2
-rw-r--r--app/models/concerns/loaded_in_group_list.rb2
-rw-r--r--app/models/concerns/mentionable.rb2
-rw-r--r--app/models/concerns/mentionable/reference_regexes.rb2
-rw-r--r--app/models/concerns/milestoneable.rb4
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb7
-rw-r--r--app/models/concerns/packages/debian/architecture.rb1
-rw-r--r--app/models/concerns/packages/debian/component.rb1
-rw-r--r--app/models/concerns/packages/debian/component_file.rb2
-rw-r--r--app/models/concerns/packages/debian/distribution.rb2
-rw-r--r--app/models/concerns/participable.rb2
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/prometheus_adapter.rb7
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/protected_ref_access.rb6
-rw-r--r--app/models/concerns/reactive_caching.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb12
-rw-r--r--app/models/concerns/repository_storage_movable.rb4
-rw-r--r--app/models/concerns/routable.rb46
-rw-r--r--app/models/concerns/services/data_fields.rb6
-rw-r--r--app/models/concerns/sha256_attribute.rb4
-rw-r--r--app/models/concerns/sha_attribute.rb6
-rw-r--r--app/models/concerns/sidebars/container_with_html_options.rb42
-rw-r--r--app/models/concerns/sidebars/has_active_routes.rb16
-rw-r--r--app/models/concerns/sidebars/has_hint.rb16
-rw-r--r--app/models/concerns/sidebars/has_icon.rb27
-rw-r--r--app/models/concerns/sidebars/has_pill.rb21
-rw-r--r--app/models/concerns/sidebars/positionable_list.rb37
-rw-r--r--app/models/concerns/sidebars/renderable.rb12
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb6
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/throttled_touch.rb2
-rw-r--r--app/models/concerns/timebox.rb6
-rw-r--r--app/models/concerns/token_authenticatable.rb2
-rw-r--r--app/models/concerns/triggerable_hooks.rb4
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb2
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb2
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb4
-rw-r--r--app/models/container_registry/event.rb2
-rw-r--r--app/models/container_repository.rb21
-rw-r--r--app/models/context_commits_diff.rb58
-rw-r--r--app/models/cycle_analytics/project_level_stage_adapter.rb4
-rw-r--r--app/models/deployment.rb15
-rw-r--r--app/models/deployment_merge_request.rb4
-rw-r--r--app/models/description_version.rb2
-rw-r--r--app/models/design_management/version.rb3
-rw-r--r--app/models/discussion_note.rb2
-rw-r--r--app/models/email.rb6
-rw-r--r--app/models/environment.rb10
-rw-r--r--app/models/epic.rb2
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/external_pull_request.rb4
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb70
-rw-r--r--app/models/hooks/project_hook.rb12
-rw-r--r--app/models/hooks/service_hook.rb8
-rw-r--r--app/models/hooks/web_hook.rb48
-rw-r--r--app/models/hooks/web_hook_log.rb3
-rw-r--r--app/models/hooks/web_hook_log_archived.rb12
-rw-r--r--app/models/hooks/web_hook_log_partitioned.rb17
-rw-r--r--app/models/identity.rb2
-rw-r--r--app/models/identity/uniqueness_scopes.rb2
-rw-r--r--app/models/incident_management/project_incident_management_setting.rb4
-rw-r--r--app/models/instance_metadata.rb3
-rw-r--r--app/models/instance_metadata/kas.rb15
-rw-r--r--app/models/integration.rb (renamed from app/models/service.rb)140
-rw-r--r--app/models/integrations/asana.rb109
-rw-r--r--app/models/integrations/assembla.rb38
-rw-r--r--app/models/integrations/bamboo.rb183
-rw-r--r--app/models/integrations/builds_email.rb16
-rw-r--r--app/models/integrations/campfire.rb104
-rw-r--r--app/models/integrations/chat_message/alert_message.rb76
-rw-r--r--app/models/integrations/chat_message/base_message.rb88
-rw-r--r--app/models/integrations/chat_message/deployment_message.rb87
-rw-r--r--app/models/integrations/chat_message/issue_message.rb74
-rw-r--r--app/models/integrations/chat_message/merge_message.rb83
-rw-r--r--app/models/integrations/chat_message/note_message.rb86
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb267
-rw-r--r--app/models/integrations/chat_message/push_message.rb120
-rw-r--r--app/models/integrations/chat_message/wiki_page_message.rb63
-rw-r--r--app/models/integrations/confluence.rb93
-rw-r--r--app/models/integrations/datadog.rb143
-rw-r--r--app/models/integrations/emails_on_push.rb99
-rw-r--r--app/models/issue.rb30
-rw-r--r--app/models/issue/metrics.rb6
-rw-r--r--app/models/issue_assignee.rb4
-rw-r--r--app/models/issue_link.rb2
-rw-r--r--app/models/iteration.rb2
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/label_link.rb3
-rw-r--r--app/models/label_note.rb2
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb21
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/members_preloader.rb9
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/merge_request/metrics.rb2
-rw-r--r--app/models/merge_request_assignee.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb4
-rw-r--r--app/models/merge_request_diff.rb40
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/milestone_release.rb2
-rw-r--r--app/models/namespace.rb25
-rw-r--r--app/models/namespace/package_setting.rb6
-rw-r--r--app/models/namespace/root_storage_statistics.rb2
-rw-r--r--app/models/namespace/traversal_hierarchy.rb24
-rw-r--r--app/models/namespace_setting.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb87
-rw-r--r--app/models/network/graph.rb2
-rw-r--r--app/models/note.rb7
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/operations/feature_flag.rb2
-rw-r--r--app/models/packages.rb2
-rw-r--r--app/models/packages/debian/group_distribution.rb10
-rw-r--r--app/models/packages/debian/project_distribution.rb5
-rw-r--r--app/models/packages/go/module.rb4
-rw-r--r--app/models/packages/go/module_version.rb12
-rw-r--r--app/models/packages/helm.rb9
-rw-r--r--app/models/packages/helm/file_metadatum.rb30
-rw-r--r--app/models/packages/package.rb40
-rw-r--r--app/models/packages/package_file.rb20
-rw-r--r--app/models/pages/lookup_path.rb11
-rw-r--r--app/models/pages/virtual_domain.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/pages_domain_acme_order.rb2
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/models/plan.rb2
-rw-r--r--app/models/pool_repository.rb2
-rw-r--r--app/models/preloaders/labels_preloader.rb2
-rw-r--r--app/models/project.rb143
-rw-r--r--app/models/project_authorization.rb2
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_feature_usage.rb2
-rw-r--r--app/models/project_group_link.rb3
-rw-r--r--app/models/project_import_data.rb2
-rw-r--r--app/models/project_import_state.rb2
-rw-r--r--app/models/project_services/asana_service.rb107
-rw-r--r--app/models/project_services/assembla_service.rb36
-rw-r--r--app/models/project_services/bamboo_service.rb181
-rw-r--r--app/models/project_services/bugzilla_service.rb9
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/builds_email_service.rb13
-rw-r--r--app/models/project_services/campfire_service.rb102
-rw-r--r--app/models/project_services/chat_message/alert_message.rb74
-rw-r--r--app/models/project_services/chat_message/base_message.rb86
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb85
-rw-r--r--app/models/project_services/chat_message/issue_message.rb72
-rw-r--r--app/models/project_services/chat_message/merge_message.rb81
-rw-r--r--app/models/project_services/chat_message/note_message.rb84
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb265
-rw-r--r--app/models/project_services/chat_message/push_message.rb118
-rw-r--r--app/models/project_services/chat_message/wiki_page_message.rb61
-rw-r--r--app/models/project_services/chat_notification_service.rb54
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_services/confluence_service.rb91
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb18
-rw-r--r--app/models/project_services/data_fields.rb6
-rw-r--r--app/models/project_services/datadog_service.rb144
-rw-r--r--app/models/project_services/emails_on_push_service.rb97
-rw-r--r--app/models/project_services/ewm_service.rb9
-rw-r--r--app/models/project_services/external_wiki_service.rb5
-rw-r--r--app/models/project_services/flowdock_service.rb13
-rw-r--r--app/models/project_services/hangouts_chat_service.rb17
-rw-r--r--app/models/project_services/hipchat_service.rb143
-rw-r--r--app/models/project_services/irker_service.rb7
-rw-r--r--app/models/project_services/issue_tracker_service.rb12
-rw-r--r--app/models/project_services/jenkins_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb13
-rw-r--r--app/models/project_services/microsoft_teams_service.rb2
-rw-r--r--app/models/project_services/monitoring_service.rb2
-rw-r--r--app/models/project_services/packagist_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb2
-rw-r--r--app/models/project_services/slash_commands_service.rb2
-rw-r--r--app/models/project_services/unify_circuit_service.rb2
-rw-r--r--app/models/project_services/webex_teams_service.rb17
-rw-r--r--app/models/project_services/youtrack_service.rb13
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/push_event_payload.rb2
-rw-r--r--app/models/release.rb13
-rw-r--r--app/models/release_highlight.rb36
-rw-r--r--app/models/releases/evidence.rb2
-rw-r--r--app/models/releases/link.rb2
-rw-r--r--app/models/remote_mirror.rb6
-rw-r--r--app/models/repository.rb26
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/resource_state_event.rb2
-rw-r--r--app/models/resource_timebox_event.rb2
-rw-r--r--app/models/serverless/domain_cluster.rb2
-rw-r--r--app/models/service_list.rb2
-rw-r--r--app/models/sidebars/context.rb21
-rw-r--r--app/models/sidebars/menu.rb82
-rw-r--r--app/models/sidebars/menu_item.rb21
-rw-r--r--app/models/sidebars/panel.rb75
-rw-r--r--app/models/sidebars/projects/context.rb11
-rw-r--r--app/models/sidebars/projects/menus/learn_gitlab/menu.rb41
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu.rb45
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb35
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/details.rb36
-rw-r--r--app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb40
-rw-r--r--app/models/sidebars/projects/menus/repository/menu.rb59
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/branches.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/commits.rb35
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/compare.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/contributors.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/files.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/graphs.rb28
-rw-r--r--app/models/sidebars/projects/menus/repository/menu_items/tags.rb28
-rw-r--r--app/models/sidebars/projects/menus/scope/menu.rb21
-rw-r--r--app/models/sidebars/projects/panel.rb26
-rw-r--r--app/models/snippet.rb13
-rw-r--r--app/models/snippet_repository.rb2
-rw-r--r--app/models/ssh_host_key.rb4
-rw-r--r--app/models/storage/legacy_project.rb2
-rw-r--r--app/models/system_note_metadata.rb2
-rw-r--r--app/models/terraform/state.rb2
-rw-r--r--app/models/terraform/state_version.rb2
-rw-r--r--app/models/timelog.rb15
-rw-r--r--app/models/todo.rb6
-rw-r--r--app/models/upload.rb2
-rw-r--r--app/models/user.rb68
-rw-r--r--app/models/user_callout.rb4
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/users/credit_card_validation.rb11
-rw-r--r--app/models/users/merge_request_interaction.rb2
-rw-r--r--app/models/users_statistics.rb2
-rw-r--r--app/models/vulnerability.rb2
-rw-r--r--app/models/wiki.rb15
-rw-r--r--app/models/wiki_page.rb13
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/ci/build_policy.rb9
-rw-r--r--app/policies/ci/stage_policy.rb7
-rw-r--r--app/policies/clusters/instance_policy.rb2
-rw-r--r--app/policies/concerns/policy_actor.rb2
-rw-r--r--app/policies/concerns/readonly_abilities.rb3
-rw-r--r--app/policies/environment_policy.rb2
-rw-r--r--app/policies/global_policy.rb2
-rw-r--r--app/policies/group_member_policy.rb2
-rw-r--r--app/policies/group_policy.rb2
-rw-r--r--app/policies/identity_provider_policy.rb2
-rw-r--r--app/policies/integration_policy.rb (renamed from app/policies/service_policy.rb)2
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/issue_policy.rb2
-rw-r--r--app/policies/merge_request_policy.rb2
-rw-r--r--app/policies/namespace_policy.rb2
-rw-r--r--app/policies/nil_policy.rb5
-rw-r--r--app/policies/packages/maven/metadatum_policy.rb8
-rw-r--r--app/policies/packages/nuget/metadatum_policy.rb8
-rw-r--r--app/policies/project_member_policy.rb6
-rw-r--r--app/policies/project_policy.rb5
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/policies/protected_branch_policy.rb2
-rw-r--r--app/policies/user_policy.rb2
-rw-r--r--app/presenters/alert_management/alert_presenter.rb2
-rw-r--r--app/presenters/blob_presenter.rb62
-rw-r--r--app/presenters/ci/build_presenter.rb2
-rw-r--r--app/presenters/ci/build_runner_presenter.rb2
-rw-r--r--app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb10
-rw-r--r--app/presenters/ci/pipeline_presenter.rb21
-rw-r--r--app/presenters/clusterable_presenter.rb2
-rw-r--r--app/presenters/clusters/cluster_presenter.rb4
-rw-r--r--app/presenters/commit_status_presenter.rb6
-rw-r--r--app/presenters/group_clusterable_presenter.rb2
-rw-r--r--app/presenters/group_member_presenter.rb2
-rw-r--r--app/presenters/instance_clusterable_presenter.rb2
-rw-r--r--app/presenters/issue_presenter.rb2
-rw-r--r--app/presenters/label_presenter.rb2
-rw-r--r--app/presenters/member_presenter.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb13
-rw-r--r--app/presenters/packages/detail/package_presenter.rb1
-rw-r--r--app/presenters/project_clusterable_presenter.rb2
-rw-r--r--app/presenters/project_member_presenter.rb2
-rw-r--r--app/presenters/project_presenter.rb46
-rw-r--r--app/presenters/projects/import_export/project_export_presenter.rb4
-rw-r--r--app/presenters/service_hook_presenter.rb4
-rw-r--r--app/presenters/snippet_blob_presenter.rb6
-rw-r--r--app/presenters/terraform/modules_presenter.rb40
-rw-r--r--app/serializers/admin/user_entity.rb2
-rw-r--r--app/serializers/analytics/cycle_analytics/configuration_entity.rb22
-rw-r--r--app/serializers/analytics/cycle_analytics/event_entity.rb37
-rw-r--r--app/serializers/analytics/cycle_analytics/stage_entity.rb38
-rw-r--r--app/serializers/analytics/cycle_analytics/value_stream_entity.rb24
-rw-r--r--app/serializers/analytics/cycle_analytics/value_stream_serializer.rb9
-rw-r--r--app/serializers/blob_entity.rb2
-rw-r--r--app/serializers/board_simple_entity.rb2
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/ci/downloadable_artifact_entity.rb17
-rw-r--r--app/serializers/ci/downloadable_artifact_serializer.rb7
-rw-r--r--app/serializers/ci/pipeline_entity.rb2
-rw-r--r--app/serializers/cluster_entity.rb2
-rw-r--r--app/serializers/context_commits_diff_entity.rb20
-rw-r--r--app/serializers/current_board_entity.rb2
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/diffs_entity.rb9
-rw-r--r--app/serializers/discussion_serializer.rb2
-rw-r--r--app/serializers/environment_entity.rb4
-rw-r--r--app/serializers/environment_serializer.rb6
-rw-r--r--app/serializers/evidences/release_entity.rb2
-rw-r--r--app/serializers/fork_namespace_entity.rb2
-rw-r--r--app/serializers/group_child_entity.rb2
-rw-r--r--app/serializers/group_issuable_autocomplete_entity.rb9
-rw-r--r--app/serializers/group_issuable_autocomplete_serializer.rb5
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb2
-rw-r--r--app/serializers/issue_board_entity.rb4
-rw-r--r--app/serializers/issue_entity.rb2
-rw-r--r--app/serializers/issue_sidebar_basic_entity.rb2
-rw-r--r--app/serializers/issue_sidebar_extras_entity.rb2
-rw-r--r--app/serializers/job_entity.rb1
-rw-r--r--app/serializers/member_entity.rb2
-rw-r--r--app/serializers/member_user_entity.rb2
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb2
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb2
-rw-r--r--app/serializers/merge_request_serializer.rb2
-rw-r--r--app/serializers/merge_request_user_entity.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb4
-rw-r--r--app/serializers/note_entity.rb2
-rw-r--r--app/serializers/note_user_entity.rb2
-rw-r--r--app/serializers/pipeline_details_entity.rb14
-rw-r--r--app/serializers/pipeline_serializer.rb6
-rw-r--r--app/serializers/project_mirror_entity.rb2
-rw-r--r--app/serializers/test_case_entity.rb2
-rw-r--r--app/serializers/user_entity.rb2
-rw-r--r--app/serializers/user_preference_entity.rb2
-rw-r--r--app/serializers/user_serializer.rb2
-rw-r--r--app/services/admin/propagate_integration_service.rb6
-rw-r--r--app/services/alert_management/http_integrations/create_service.rb2
-rw-r--r--app/services/alert_management/http_integrations/update_service.rb2
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb7
-rw-r--r--app/services/analytics/cycle_analytics/stages/base_service.rb47
-rw-r--r--app/services/analytics/cycle_analytics/stages/list_service.rb27
-rw-r--r--app/services/application_settings/update_service.rb2
-rw-r--r--app/services/applications/create_service.rb2
-rw-r--r--app/services/audit_event_service.rb4
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/auto_merge/base_service.rb6
-rw-r--r--app/services/auto_merge_service.rb2
-rw-r--r--app/services/award_emojis/add_service.rb2
-rw-r--r--app/services/award_emojis/destroy_service.rb2
-rw-r--r--app/services/base_container_service.rb9
-rw-r--r--app/services/base_count_service.rb2
-rw-r--r--app/services/base_project_service.rb14
-rw-r--r--app/services/base_service.rb9
-rw-r--r--app/services/boards/base_items_list_service.rb2
-rw-r--r--app/services/boards/base_service.rb2
-rw-r--r--app/services/boards/create_service.rb2
-rw-r--r--app/services/boards/issues/create_service.rb4
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/boards/lists/base_destroy_service.rb34
-rw-r--r--app/services/boards/lists/base_update_service.rb16
-rw-r--r--app/services/boards/lists/create_service.rb2
-rw-r--r--app/services/boards/lists/destroy_service.rb32
-rw-r--r--app/services/boards/lists/list_service.rb2
-rw-r--r--app/services/boards/lists/update_service.rb2
-rw-r--r--app/services/boards/update_service.rb2
-rw-r--r--app/services/boards/visits/create_service.rb16
-rw-r--r--app/services/bulk_create_integration_service.rb2
-rw-r--r--app/services/bulk_imports/export_service.rb27
-rw-r--r--app/services/bulk_imports/relation_export_service.rb106
-rw-r--r--app/services/bulk_update_integration_service.rb4
-rw-r--r--app/services/chat_names/find_user_service.rb6
-rw-r--r--app/services/ci/after_requeue_job_service.rb6
-rw-r--r--app/services/ci/archive_trace_service.rb2
-rw-r--r--app/services/ci/change_variable_service.rb2
-rw-r--r--app/services/ci/change_variables_service.rb2
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb19
-rw-r--r--app/services/ci/create_pipeline_service.rb10
-rw-r--r--app/services/ci/create_web_ide_terminal_service.rb5
-rw-r--r--app/services/ci/delete_unit_tests_service.rb37
-rw-r--r--app/services/ci/expire_pipeline_cache_service.rb5
-rw-r--r--app/services/ci/generate_codequality_mr_diff_report_service.rb4
-rw-r--r--app/services/ci/generate_coverage_reports_service.rb2
-rw-r--r--app/services/ci/generate_exposed_artifacts_report_service.rb2
-rw-r--r--app/services/ci/generate_terraform_reports_service.rb2
-rw-r--r--app/services/ci/job_artifacts/create_service.rb2
-rw-r--r--app/services/ci/job_artifacts/destroy_associations_service.rb30
-rw-r--r--app/services/ci/job_artifacts/destroy_batch_service.rb31
-rw-r--r--app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb59
-rw-r--r--app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb2
-rw-r--r--app/services/ci/pipeline_bridge_status_service.rb2
-rw-r--r--app/services/ci/pipeline_trigger_service.rb20
-rw-r--r--app/services/ci/prepare_build_service.rb2
-rw-r--r--app/services/ci/process_build_service.rb2
-rw-r--r--app/services/ci/process_pipeline_service.rb8
-rw-r--r--app/services/ci/prometheus_metrics/observe_histograms_service.rb6
-rw-r--r--app/services/ci/register_job_service.rb26
-rw-r--r--app/services/ci/retry_build_service.rb14
-rw-r--r--app/services/ci/retry_pipeline_service.rb4
-rw-r--r--app/services/ci/stop_environments_service.rb4
-rw-r--r--app/services/ci/test_failure_history_service.rb2
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb4
-rw-r--r--app/services/clusters/applications/check_upgrade_progress_service.rb2
-rw-r--r--app/services/clusters/applications/prometheus_config_service.rb2
-rw-r--r--app/services/clusters/applications/prometheus_update_service.rb3
-rw-r--r--app/services/clusters/applications/schedule_update_service.rb1
-rw-r--r--app/services/clusters/aws/fetch_credentials_service.rb4
-rw-r--r--app/services/clusters/integrations/create_service.rb15
-rw-r--r--app/services/clusters/management/create_project_service.rb84
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/concerns/alert_management/alert_processing.rb26
-rw-r--r--app/services/concerns/integrations/project_test_data.rb2
-rw-r--r--app/services/concerns/measurable.rb2
-rw-r--r--app/services/concerns/services/return_service_responses.rb15
-rw-r--r--app/services/container_expiration_policies/cleanup_service.rb31
-rw-r--r--app/services/deploy_keys/create_service.rb2
-rw-r--r--app/services/deployments/older_deployments_drop_service.rb2
-rw-r--r--app/services/deployments/update_environment_service.rb2
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb2
-rw-r--r--app/services/design_management/delete_designs_service.rb2
-rw-r--r--app/services/design_management/save_designs_service.rb2
-rw-r--r--app/services/discussions/resolve_service.rb2
-rw-r--r--app/services/draft_notes/publish_service.rb6
-rw-r--r--app/services/emails/base_service.rb2
-rw-r--r--app/services/emails/create_service.rb2
-rw-r--r--app/services/emails/destroy_service.rb2
-rw-r--r--app/services/error_tracking/issue_update_service.rb2
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/git/branch_hooks_service.rb3
-rw-r--r--app/services/git/branch_push_service.rb2
-rw-r--r--app/services/git/process_ref_changes_service.rb2
-rw-r--r--app/services/git/tag_hooks_service.rb2
-rw-r--r--app/services/git/wiki_push_service.rb2
-rw-r--r--app/services/groups/autocomplete_service.rb49
-rw-r--r--app/services/groups/count_service.rb23
-rw-r--r--app/services/groups/create_service.rb4
-rw-r--r--app/services/groups/destroy_service.rb2
-rw-r--r--app/services/groups/import_export/export_service.rb4
-rw-r--r--app/services/groups/import_export/import_service.rb4
-rw-r--r--app/services/groups/open_issues_count_service.rb15
-rw-r--r--app/services/groups/participants_service.rb31
-rw-r--r--app/services/groups/transfer_service.rb8
-rw-r--r--app/services/groups/update_service.rb2
-rw-r--r--app/services/ide/schemas_config_service.rb4
-rw-r--r--app/services/import/base_service.rb2
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/import/gitlab_projects/create_project_from_remote_file_service.rb74
-rw-r--r--app/services/import/gitlab_projects/create_project_from_uploaded_file_service.rb65
-rw-r--r--app/services/incident_management/incidents/create_service.rb12
-rw-r--r--app/services/integrations/test/project_service.rb2
-rw-r--r--app/services/issuable/bulk_update_service.rb23
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb9
-rw-r--r--app/services/issuable/clone/base_service.rb4
-rw-r--r--app/services/issuable/common_system_notes_service.rb4
-rw-r--r--app/services/issuable/destroy_label_links_service.rb35
-rw-r--r--app/services/issuable/destroy_service.rb30
-rw-r--r--app/services/issuable/import_csv/base_service.rb2
-rw-r--r--app/services/issuable_base_service.rb26
-rw-r--r--app/services/issuable_links/create_service.rb2
-rw-r--r--app/services/issue_links/create_service.rb2
-rw-r--r--app/services/issue_rebalancing_service.rb50
-rw-r--r--app/services/issues/after_create_service.rb2
-rw-r--r--app/services/issues/base_service.rb9
-rw-r--r--app/services/issues/build_service.rb23
-rw-r--r--app/services/issues/clone_service.rb4
-rw-r--r--app/services/issues/close_service.rb16
-rw-r--r--app/services/issues/create_service.rb4
-rw-r--r--app/services/issues/duplicate_service.rb2
-rw-r--r--app/services/issues/export_csv_service.rb2
-rw-r--r--app/services/issues/move_service.rb4
-rw-r--r--app/services/issues/related_branches_service.rb2
-rw-r--r--app/services/issues/reorder_service.rb2
-rw-r--r--app/services/issues/update_service.rb38
-rw-r--r--app/services/issues/zoom_link_service.rb6
-rw-r--r--app/services/jira_import/start_import_service.rb2
-rw-r--r--app/services/keys/create_service.rb2
-rw-r--r--app/services/keys/destroy_service.rb2
-rw-r--r--app/services/labels/available_labels_service.rb20
-rw-r--r--app/services/labels/create_service.rb2
-rw-r--r--app/services/labels/find_or_create_service.rb13
-rw-r--r--app/services/labels/promote_service.rb2
-rw-r--r--app/services/lfs/lock_file_service.rb4
-rw-r--r--app/services/lfs/locks_finder_service.rb2
-rw-r--r--app/services/lfs/push_service.rb7
-rw-r--r--app/services/lfs/unlock_file_service.rb4
-rw-r--r--app/services/members/approve_access_request_service.rb2
-rw-r--r--app/services/members/create_service.rb2
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/members/update_service.rb2
-rw-r--r--app/services/merge_request_metrics_service.rb2
-rw-r--r--app/services/merge_requests/add_context_service.rb2
-rw-r--r--app/services/merge_requests/add_spent_time_service.rb34
-rw-r--r--app/services/merge_requests/after_create_service.rb4
-rw-r--r--app/services/merge_requests/approval_service.rb2
-rw-r--r--app/services/merge_requests/assign_issues_service.rb4
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb28
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb2
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb8
-rw-r--r--app/services/merge_requests/handle_assignees_change_service.rb20
-rw-r--r--app/services/merge_requests/link_lfs_objects_service.rb2
-rw-r--r--app/services/merge_requests/merge_base_service.rb4
-rw-r--r--app/services/merge_requests/merge_service.rb22
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb2
-rw-r--r--app/services/merge_requests/post_merge_service.rb20
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb30
-rw-r--r--app/services/merge_requests/rebase_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb17
-rw-r--r--app/services/merge_requests/remove_approval_service.rb2
-rw-r--r--app/services/merge_requests/resolve_todos_service.rb6
-rw-r--r--app/services/merge_requests/retarget_chain_service.rb8
-rw-r--r--app/services/merge_requests/squash_service.rb4
-rw-r--r--app/services/merge_requests/update_assignees_service.rb6
-rw-r--r--app/services/merge_requests/update_service.rb14
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb6
-rw-r--r--app/services/metrics/dashboard/transient_embed_service.rb2
-rw-r--r--app/services/metrics/dashboard/update_dashboard_service.rb2
-rw-r--r--app/services/milestones/destroy_service.rb4
-rw-r--r--app/services/milestones/promote_service.rb2
-rw-r--r--app/services/milestones/update_service.rb2
-rw-r--r--app/services/namespace_settings/update_service.rb2
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb14
-rw-r--r--app/services/namespaces/package_settings/update_service.rb5
-rw-r--r--app/services/namespaces/statistics_refresher_service.rb2
-rw-r--r--app/services/notes/create_service.rb2
-rw-r--r--app/services/notes/destroy_service.rb2
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notes/quick_actions_service.rb22
-rw-r--r--app/services/notes/resolve_service.rb2
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_recipients/builder/base.rb26
-rw-r--r--app/services/notification_recipients/builder/default.rb2
-rw-r--r--app/services/notification_service.rb3
-rw-r--r--app/services/packages/debian/extract_changes_metadata_service.rb20
-rw-r--r--app/services/packages/debian/extract_metadata_service.rb2
-rw-r--r--app/services/packages/debian/generate_distribution_key_service.rb106
-rw-r--r--app/services/packages/debian/generate_distribution_service.rb207
-rw-r--r--app/services/packages/generic/create_package_file_service.rb8
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb2
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb6
-rw-r--r--app/services/packages/nuget/search_service.rb1
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb6
-rw-r--r--app/services/packages/pypi/create_package_service.rb2
-rw-r--r--app/services/packages/rubygems/process_gem_service.rb7
-rw-r--r--app/services/packages/terraform_module/create_package_service.rb70
-rw-r--r--app/services/pages/migrate_from_legacy_storage_service.rb4
-rw-r--r--app/services/pages/zip_directory_service.rb2
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb2
-rw-r--r--app/services/personal_access_tokens/create_service.rb2
-rw-r--r--app/services/personal_access_tokens/revoke_service.rb2
-rw-r--r--app/services/pod_logs/elasticsearch_service.rb8
-rw-r--r--app/services/post_receive_service.rb6
-rw-r--r--app/services/preview_markdown_service.rb2
-rw-r--r--app/services/projects/after_rename_service.rb8
-rw-r--r--app/services/projects/autocomplete_service.rb2
-rw-r--r--app/services/projects/cleanup_service.rb2
-rw-r--r--app/services/projects/create_from_template_service.rb2
-rw-r--r--app/services/projects/create_service.rb17
-rw-r--r--app/services/projects/destroy_service.rb24
-rw-r--r--app/services/projects/disable_deploy_key_service.rb2
-rw-r--r--app/services/projects/enable_deploy_key_service.rb2
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb2
-rw-r--r--app/services/projects/group_links/create_service.rb2
-rw-r--r--app/services/projects/group_links/destroy_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb2
-rw-r--r--app/services/projects/housekeeping_service.rb16
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/import_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb2
-rw-r--r--app/services/projects/operations/update_service.rb2
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb18
-rw-r--r--app/services/projects/transfer_service.rb14
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/services/projects/update_pages_configuration_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb6
-rw-r--r--app/services/projects/update_remote_mirror_service.rb2
-rw-r--r--app/services/projects/update_service.rb8
-rw-r--r--app/services/projects/update_statistics_service.rb35
-rw-r--r--app/services/prometheus/create_default_alerts_service.rb2
-rw-r--r--app/services/protected_branches/access_level_params.rb2
-rw-r--r--app/services/protected_branches/api_service.rb2
-rw-r--r--app/services/protected_branches/create_service.rb2
-rw-r--r--app/services/protected_branches/destroy_service.rb2
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb2
-rw-r--r--app/services/protected_branches/update_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb2
-rw-r--r--app/services/quick_actions/target_service.rb2
-rw-r--r--app/services/releases/base_service.rb2
-rw-r--r--app/services/releases/create_evidence_service.rb2
-rw-r--r--app/services/releases/create_service.rb2
-rw-r--r--app/services/repositories/changelog_service.rb2
-rw-r--r--app/services/resource_access_tokens/create_service.rb2
-rw-r--r--app/services/resource_access_tokens/revoke_service.rb2
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb2
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/services/search/group_service.rb2
-rw-r--r--app/services/search/project_service.rb2
-rw-r--r--app/services/search/snippet_service.rb2
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/security/ci_configuration/base_create_service.rb62
-rw-r--r--app/services/security/ci_configuration/sast_create_service.rb58
-rw-r--r--app/services/security/ci_configuration/sast_parser_service.rb2
-rw-r--r--app/services/security/ci_configuration/secret_detection_create_service.rb25
-rw-r--r--app/services/service_response.rb8
-rw-r--r--app/services/snippets/bulk_destroy_service.rb2
-rw-r--r--app/services/snippets/create_service.rb2
-rw-r--r--app/services/snippets/destroy_service.rb4
-rw-r--r--app/services/snippets/update_service.rb2
-rw-r--r--app/services/spam/akismet_service.rb6
-rw-r--r--app/services/spam/spam_action_service.rb10
-rw-r--r--app/services/spam/spam_constants.rb4
-rw-r--r--app/services/spam/spam_verdict_service.rb106
-rw-r--r--app/services/static_site_editor/config_service.rb4
-rw-r--r--app/services/submit_usage_ping_service.rb8
-rw-r--r--app/services/suggestions/apply_service.rb12
-rw-r--r--app/services/system_hooks_service.rb61
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/system_notes/base_service.rb6
-rw-r--r--app/services/system_notes/issuables_service.rb9
-rw-r--r--app/services/system_notes/time_tracking_service.rb4
-rw-r--r--app/services/terraform/remote_state_handler.rb2
-rw-r--r--app/services/todo_service.rb10
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb2
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb4
-rw-r--r--app/services/user_project_access_changed_service.rb2
-rw-r--r--app/services/users/activity_service.rb2
-rw-r--r--app/services/users/approve_service.rb2
-rw-r--r--app/services/users/ban_service.rb25
-rw-r--r--app/services/users/block_service.rb2
-rw-r--r--app/services/users/build_service.rb22
-rw-r--r--app/services/users/create_service.rb2
-rw-r--r--app/services/users/destroy_service.rb4
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb2
-rw-r--r--app/services/users/registrations_build_service.rb19
-rw-r--r--app/services/users/reject_service.rb2
-rw-r--r--app/services/users/update_assigned_open_issue_count_service.rb33
-rw-r--r--app/services/users/update_canonical_email_service.rb2
-rw-r--r--app/services/users/update_service.rb4
-rw-r--r--app/services/users/update_todo_count_cache_service.rb29
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb20
-rw-r--r--app/services/verify_pages_domain_service.rb2
-rw-r--r--app/services/web_hook_service.rb64
-rw-r--r--app/services/wiki_pages/base_service.rb2
-rw-r--r--app/services/wiki_pages/update_service.rb14
-rw-r--r--app/uploaders/bulk_imports/export_uploader.rb7
-rw-r--r--app/uploaders/file_mover.rb2
-rw-r--r--app/uploaders/object_storage.rb7
-rw-r--r--app/validators/branch_filter_validator.rb4
-rw-r--r--app/validators/cron_validator.rb2
-rw-r--r--app/validators/json_schema_validator.rb2
-rw-r--r--app/validators/json_schemas/helm_metadata.json128
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json7
-rw-r--r--app/validators/same_project_association_validator.rb2
-rw-r--r--app/views/admin/appearances/_form.html.haml34
-rw-r--r--app/views/admin/appearances/preview_sign_in.html.haml8
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml6
-rw-r--r--app/views/admin/application_settings/_floc.html.haml22
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml8
-rw-r--r--app/views/admin/application_settings/_initial_branch_name.html.haml4
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml3
-rw-r--r--app/views/admin/application_settings/_package_registry_limits.html.haml37
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml22
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml22
-rw-r--r--app/views/admin/application_settings/_spam.html.haml32
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml7
-rw-r--r--app/views/admin/application_settings/_usage.html.haml11
-rw-r--r--app/views/admin/application_settings/_whats_new.html.haml13
-rw-r--r--app/views/admin/application_settings/general.html.haml1
-rw-r--r--app/views/admin/application_settings/network.html.haml11
-rw-r--r--app/views/admin/application_settings/preferences.html.haml11
-rw-r--r--app/views/admin/application_settings/repository.html.haml21
-rw-r--r--app/views/admin/background_jobs/show.html.haml7
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml10
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/dev_ops_report/_card.html.haml25
-rw-r--r--app/views/admin/dev_ops_report/_no_data.html.haml7
-rw-r--r--app/views/admin/dev_ops_report/_report.html.haml26
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/health_check/show.html.haml6
-rw-r--r--app/views/admin/hook_logs/_index.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/labels/destroy.js.haml3
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/admin/requests_profiles/index.html.haml6
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/admin/runners/show.html.haml15
-rw-r--r--app/views/admin/spam_logs/index.html.haml22
-rw-r--r--app/views/admin/users/_ban_user.html.haml9
-rw-r--r--app/views/admin/users/_cohorts.html.haml5
-rw-r--r--app/views/admin/users/_form.html.haml26
-rw-r--r--app/views/admin/users/_head.html.haml3
-rw-r--r--app/views/admin/users/_profile.html.haml16
-rw-r--r--app/views/admin/users/_projects.html.haml4
-rw-r--r--app/views/admin/users/_tabs.html.haml7
-rw-r--r--app/views/admin/users/_user.html.haml8
-rw-r--r--app/views/admin/users/_users.html.haml27
-rw-r--r--app/views/admin/users/cohorts.html.haml7
-rw-r--r--app/views/admin/users/edit.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/admin/users/projects.html.haml10
-rw-r--r--app/views/admin/users/show.html.haml115
-rw-r--r--app/views/clusters/clusters/_banner.html.haml2
-rw-r--r--app/views/clusters/clusters/_integrations.html.haml34
-rw-r--r--app/views/clusters/clusters/show.html.haml6
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/dashboard/groups/_groups.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml120
-rw-r--r--app/views/dashboard/todos/index.html.haml87
-rw-r--r--app/views/devise/confirmations/new.html.haml4
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.html.haml10
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.haml11
-rw-r--r--app/views/devise/mailer/reset_password_instructions.text.erb9
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb6
-rw-r--r--app/views/devise/shared/_links.erb10
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_omniauth_providers_top.haml2
-rw-r--r--app/views/devise/shared/_terms_of_service_notice.html.haml12
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml4
-rw-r--r--app/views/explore/groups/_groups.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/groups/_archived_projects.html.haml2
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml8
-rw-r--r--app/views/groups/_invite_members_modal.html.haml2
-rw-r--r--app/views/groups/_shared_projects.html.haml2
-rw-r--r--app/views/groups/_subgroups_and_projects.html.haml2
-rw-r--r--app/views/groups/boards/show.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml33
-rw-r--r--app/views/groups/imports/show.html.haml2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml6
-rw-r--r--app/views/groups/milestones/_header_title.html.haml2
-rw-r--r--app/views/groups/milestones/new.html.haml2
-rw-r--r--app/views/groups/runners/edit.html.haml5
-rw-r--r--app/views/groups/runners/show.html.haml3
-rw-r--r--app/views/groups/settings/_lfs.html.haml12
-rw-r--r--app/views/groups/settings/_two_factor_auth.html.haml14
-rw-r--r--app/views/groups/settings/packages_and_registries/show.html.haml (renamed from app/views/groups/settings/packages_and_registries/index.html.haml)0
-rw-r--r--app/views/groups/settings/repository/_initial_branch_name.html.haml4
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--app/views/ide/_show.html.haml5
-rw-r--r--app/views/import/bitbucket_server/new.html.haml6
-rw-r--r--app/views/import/bulk_imports/status.html.haml3
-rw-r--r--app/views/import/fogbugz/new.html.haml6
-rw-r--r--app/views/import/gitea/new.html.haml4
-rw-r--r--app/views/import/phabricator/new.html.haml4
-rw-r--r--app/views/import/shared/_new_project_form.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_loading_hints.html.haml21
-rw-r--r--app/views/layouts/_page_title.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml19
-rw-r--r--app/views/layouts/header/_new_repo_experiment.html.haml11
-rw-r--r--app/views/layouts/header/_whats_new_dropdown_item.html.haml2
-rw-r--r--app/views/layouts/nav/_combined_menu.html.haml3
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml6
-rw-r--r--app/views/layouts/nav/_explore.html.haml4
-rw-r--r--app/views/layouts/nav/_top_nav.html.haml7
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml10
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml6
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml27
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml69
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project_menus.html.haml380
-rw-r--r--app/views/layouts/nav/sidebar/_project_packages_link.html.haml27
-rw-r--r--app/views/layouts/nav/sidebar/_project_security_link.html.haml21
-rw-r--r--app/views/layouts/nav/sidebar/_tracing_link.html.haml7
-rw-r--r--app/views/layouts/simple_registration.html.haml10
-rw-r--r--app/views/notify/change_in_merge_request_draft_status_email.html.haml4
-rw-r--r--app/views/notify/in_product_marketing_email.html.haml24
-rw-r--r--app/views/notify/in_product_marketing_email.text.erb18
-rw-r--r--app/views/notify/new_issue_email.html.haml3
-rw-r--r--app/views/notify/new_issue_email.text.erb3
-rw-r--r--app/views/notify/new_merge_request_email.html.haml6
-rw-r--r--app/views/notify/new_merge_request_email.text.erb6
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml8
-rw-r--r--app/views/profiles/keys/_form.html.haml6
-rw-r--r--app/views/profiles/keys/_key.html.haml2
-rw-r--r--app/views/profiles/passwords/new.html.haml8
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/profiles/show.html.haml7
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_archived_notice.html.haml2
-rw-r--r--app/views/projects/_commit_button.html.haml4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_fork_suggestion.html.haml13
-rw-r--r--app/views/projects/_home_panel.html.haml79
-rw-r--r--app/views/projects/_import_project_pane.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_suggestions_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml6
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml20
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml3
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml9
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_changelog.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_contributing.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_license.html.haml6
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml4
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml6
-rw-r--r--app/views/projects/buttons/_remove_tag.html.haml4
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml4
-rw-r--r--app/views/projects/commit/show.html.haml7
-rw-r--r--app/views/projects/commits/_commit.html.haml26
-rw-r--r--app/views/projects/commits/_commits.html.haml14
-rw-r--r--app/views/projects/compare/index.html.haml6
-rw-r--r--app/views/projects/compare/show.html.haml11
-rw-r--r--app/views/projects/diffs/_diffs.html.haml5
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/feature_flags/edit.html.haml2
-rw-r--r--app/views/projects/feature_flags/new.html.haml2
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml4
-rw-r--r--app/views/projects/issues/_issues.html.haml3
-rw-r--r--app/views/projects/issues/_new_branch.html.haml4
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/learn_gitlab/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_description.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml82
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml6
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml4
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml8
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml8
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml92
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml6
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml10
-rw-r--r--app/views/projects/pipelines/index.html.haml8
-rw-r--r--app/views/projects/pipelines/new.html.haml58
-rw-r--r--app/views/projects/pipelines/show.html.haml4
-rw-r--r--app/views/projects/project_members/index.html.haml32
-rw-r--r--app/views/projects/project_templates/_template.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/runners/_group_runners.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml21
-rw-r--r--app/views/projects/runners/edit.html.haml5
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/settings/_archive.html.haml2
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml11
-rw-r--r--app/views/projects/settings/operations/_configuration_banner.html.haml2
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml4
-rw-r--r--app/views/projects/settings/operations/show.html.haml5
-rw-r--r--app/views/projects/settings/packages_and_registries/show.html.haml16
-rw-r--r--app/views/projects/sidebar/_issues_service_desk.html.haml3
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tags/index.html.haml22
-rw-r--r--app/views/projects/tags/new.html.haml4
-rw-r--r--app/views/projects/tags/releases/edit.html.haml4
-rw-r--r--app/views/projects/triggers/_trigger.html.haml4
-rw-r--r--app/views/registrations/invites/new.html.haml18
-rw-r--r--app/views/registrations/welcome/show.html.haml1
-rw-r--r--app/views/search/_category.html.haml14
-rw-r--r--app/views/search/results/_user.html.haml6
-rw-r--r--app/views/shared/_allow_request_access.html.haml6
-rw-r--r--app/views/shared/_commit_message_container.html.haml12
-rw-r--r--app/views/shared/_confirm_fork_modal.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml4
-rw-r--r--app/views/shared/_import_form.html.haml14
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml4
-rw-r--r--app/views/shared/access_tokens/_table.html.haml2
-rw-r--r--app/views/shared/alerts/_positioning_disabled.html.haml2
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml3
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_assignee.html.haml5
-rw-r--r--app/views/shared/builds/_tabs.html.haml8
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml4
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml6
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml12
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml4
-rw-r--r--app/views/shared/deploy_tokens/_new_deploy_token.html.haml6
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml23
-rw-r--r--app/views/shared/issuable/_status_box.html.haml6
-rw-r--r--app/views/shared/issuable/form/_title.html.haml7
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml9
-rw-r--r--app/views/shared/members/_invite_member.html.haml2
-rw-r--r--app/views/shared/milestones/_delete_button.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml4
-rw-r--r--app/views/shared/milestones/_search_form.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_tab_loading.html.haml2
-rw-r--r--app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml11
-rw-r--r--app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml4
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label.html.haml21
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml16
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml2
-rw-r--r--app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml15
-rw-r--r--app/views/shared/nav/_scope_menu.html.haml4
-rw-r--r--app/views/shared/nav/_sidebar.html.haml1
-rw-r--r--app/views/shared/nav/_sidebar_hidden_menu_item.html.haml3
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml8
-rw-r--r--app/views/shared/runners/_runner_details.html.haml (renamed from app/views/shared/runners/show.html.haml)39
-rw-r--r--app/views/shared/runners/_shared_runners_description.html.haml10
-rw-r--r--app/views/shared/snippets/_snippet.html.haml4
-rw-r--r--app/views/shared/ssh_keys/_key_delete.html.haml7
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml2
-rw-r--r--app/views/shared/users/_user.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml4
-rw-r--r--app/views/shared/wikis/history.html.haml4
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml8
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/_overview.html.haml8
-rw-r--r--app/views/users/show.html.haml8
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml662
-rw-r--r--app/workers/analytics/instance_statistics/count_job_trigger_worker.rb3
-rw-r--r--app/workers/analytics/instance_statistics/counter_job_worker.rb3
-rw-r--r--app/workers/analytics/usage_trends/count_job_trigger_worker.rb3
-rw-r--r--app/workers/analytics/usage_trends/counter_job_worker.rb3
-rw-r--r--app/workers/approve_blocked_pending_approval_users_worker.rb3
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/authorized_keys_worker.rb2
-rw-r--r--app/workers/authorized_project_update/periodic_recalculate_worker.rb2
-rw-r--r--app/workers/authorized_project_update/project_create_worker.rb2
-rw-r--r--app/workers/authorized_project_update/project_group_link_create_worker.rb2
-rw-r--r--app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb11
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/auto_devops/disable_worker.rb2
-rw-r--r--app/workers/auto_merge_process_worker.rb2
-rw-r--r--app/workers/background_migration_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb5
-rw-r--r--app/workers/build_hooks_worker.rb12
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/bulk_import_worker.rb4
-rw-r--r--app/workers/bulk_imports/entity_worker.rb3
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb33
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb3
-rw-r--r--app/workers/bulk_imports/relation_export_worker.rb27
-rw-r--r--app/workers/chaos/cpu_spin_worker.rb2
-rw-r--r--app/workers/chaos/db_spin_worker.rb2
-rw-r--r--app/workers/chaos/leak_mem_worker.rb2
-rw-r--r--app/workers/chaos/sleep_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb2
-rw-r--r--app/workers/ci/build_prepare_worker.rb2
-rw-r--r--app/workers/ci/build_schedule_worker.rb2
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb2
-rw-r--r--app/workers/ci/create_cross_project_pipeline_worker.rb1
-rw-r--r--app/workers/ci/daily_build_group_report_results_worker.rb2
-rw-r--r--app/workers/ci/delete_objects_worker.rb3
-rw-r--r--app/workers/ci/delete_unit_tests_worker.rb18
-rw-r--r--app/workers/ci/drop_pipeline_worker.rb4
-rw-r--r--app/workers/ci/initial_pipeline_process_worker.rb2
-rw-r--r--app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb5
-rw-r--r--app/workers/ci/pipeline_artifacts/coverage_report_worker.rb3
-rw-r--r--app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb5
-rw-r--r--app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb3
-rw-r--r--app/workers/ci/pipeline_bridge_status_worker.rb1
-rw-r--r--app/workers/ci/pipeline_success_unlock_artifacts_worker.rb2
-rw-r--r--app/workers/ci/ref_delete_unlock_artifacts_worker.rb2
-rw-r--r--app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb2
-rw-r--r--app/workers/ci/retry_pipeline_worker.rb19
-rw-r--r--app/workers/ci/schedule_delete_objects_cron_worker.rb3
-rw-r--r--app/workers/ci/test_failure_history_worker.rb4
-rw-r--r--app/workers/ci_platform_metrics_update_cron_worker.rb2
-rw-r--r--app/workers/cleanup_container_repository_worker.rb2
-rw-r--r--app/workers/cluster_configure_istio_worker.rb2
-rw-r--r--app/workers/cluster_install_app_worker.rb2
-rw-r--r--app/workers/cluster_patch_app_worker.rb2
-rw-r--r--app/workers/cluster_provision_worker.rb2
-rw-r--r--app/workers/cluster_update_app_worker.rb2
-rw-r--r--app/workers/cluster_upgrade_app_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_update_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb2
-rw-r--r--app/workers/clusters/applications/activate_service_worker.rb2
-rw-r--r--app/workers/clusters/applications/check_prometheus_health_worker.rb2
-rw-r--r--app/workers/clusters/applications/deactivate_service_worker.rb2
-rw-r--r--app/workers/clusters/applications/uninstall_worker.rb2
-rw-r--r--app/workers/clusters/applications/wait_for_uninstall_app_worker.rb2
-rw-r--r--app/workers/concerns/application_worker.rb13
-rw-r--r--app/workers/concerns/chaos_queue.rb1
-rw-r--r--app/workers/concerns/git_garbage_collect_methods.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb22
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb2
-rw-r--r--app/workers/concerns/gitlab/jira_import/import_worker.rb2
-rw-r--r--app/workers/concerns/limited_capacity/job_tracker.rb42
-rw-r--r--app/workers/concerns/limited_capacity/worker.rb86
-rw-r--r--app/workers/concerns/reactive_cacheable_worker.rb2
-rw-r--r--app/workers/concerns/waitable_worker.rb2
-rw-r--r--app/workers/concerns/worker_attributes.rb26
-rw-r--r--app/workers/container_expiration_policies/cleanup_container_repository_worker.rb131
-rw-r--r--app/workers/container_expiration_policy_worker.rb2
-rw-r--r--app/workers/create_commit_signature_worker.rb4
-rw-r--r--app/workers/create_note_diff_file_worker.rb2
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/database/batched_background_migration_worker.rb11
-rw-r--r--app/workers/delete_container_repository_worker.rb2
-rw-r--r--app/workers/delete_diff_files_worker.rb2
-rw-r--r--app/workers/delete_merged_branches_worker.rb2
-rw-r--r--app/workers/delete_stored_files_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/deployments/drop_older_deployments_worker.rb3
-rw-r--r--app/workers/deployments/execute_hooks_worker.rb5
-rw-r--r--app/workers/deployments/finished_worker.rb4
-rw-r--r--app/workers/deployments/forward_deployment_worker.rb2
-rw-r--r--app/workers/deployments/hooks_worker.rb18
-rw-r--r--app/workers/deployments/link_merge_request_worker.rb2
-rw-r--r--app/workers/deployments/success_worker.rb2
-rw-r--r--app/workers/deployments/update_environment_worker.rb2
-rw-r--r--app/workers/design_management/copy_design_collection_worker.rb3
-rw-r--r--app/workers/design_management/new_version_worker.rb2
-rw-r--r--app/workers/destroy_pages_deployments_worker.rb1
-rw-r--r--app/workers/disallow_two_factor_for_group_worker.rb3
-rw-r--r--app/workers/disallow_two_factor_for_subgroups_worker.rb3
-rw-r--r--app/workers/email_receiver_worker.rb77
-rw-r--r--app/workers/emails_on_push_worker.rb4
-rw-r--r--app/workers/environments/auto_stop_cron_worker.rb2
-rw-r--r--app/workers/environments/canary_ingress/update_worker.rb1
-rw-r--r--app/workers/error_tracking_issue_link_worker.rb2
-rw-r--r--app/workers/experiments/record_conversion_event_worker.rb3
-rw-r--r--app/workers/expire_build_artifacts_worker.rb2
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb2
-rw-r--r--app/workers/expire_job_cache_worker.rb2
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/export_csv_worker.rb2
-rw-r--r--app/workers/flush_counter_increments_worker.rb3
-rw-r--r--app/workers/git_garbage_collect_worker.rb19
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/import_pull_request_review_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_base_data_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb2
-rw-r--r--app/workers/gitlab/import/stuck_import_job.rb2
-rw-r--r--app/workers/gitlab/jira_import/advance_stage_worker.rb2
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb4
-rw-r--r--app/workers/gitlab/jira_import/stage/start_import_worker.rb2
-rw-r--r--app/workers/gitlab/phabricator_import/import_tasks_worker.rb2
-rw-r--r--app/workers/gitlab_performance_bar_stats_worker.rb3
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb4
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb3
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb3
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb3
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb3
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb2
-rw-r--r--app/workers/import_issues_csv_worker.rb2
-rw-r--r--app/workers/incident_management/add_severity_system_note_worker.rb3
-rw-r--r--app/workers/incident_management/pager_duty/process_incident_worker.rb2
-rw-r--r--app/workers/incident_management/process_alert_worker.rb8
-rw-r--r--app/workers/incident_management/process_alert_worker_v2.rb47
-rw-r--r--app/workers/incident_management/process_prometheus_alert_worker.rb2
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb2
-rw-r--r--app/workers/irker_worker.rb2
-rw-r--r--app/workers/issuable/label_links_destroy_worker.rb14
-rw-r--r--app/workers/issuable_export_csv_worker.rb4
-rw-r--r--app/workers/issuables/clear_groups_issue_counter_worker.rb29
-rw-r--r--app/workers/issue_due_scheduler_worker.rb2
-rw-r--r--app/workers/issue_placement_worker.rb9
-rw-r--r--app/workers/issue_rebalancing_worker.rb8
-rw-r--r--app/workers/jira_connect/sync_branch_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_builds_worker.rb3
-rw-r--r--app/workers/jira_connect/sync_deployments_worker.rb3
-rw-r--r--app/workers/jira_connect/sync_feature_flags_worker.rb3
-rw-r--r--app/workers/jira_connect/sync_merge_request_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_project_worker.rb3
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb2
-rw-r--r--app/workers/member_invitation_reminder_emails_worker.rb3
-rw-r--r--app/workers/members_destroyer/unassign_issuables_worker.rb2
-rw-r--r--app/workers/merge_request_cleanup_refs_worker.rb3
-rw-r--r--app/workers/merge_request_mergeability_check_worker.rb2
-rw-r--r--app/workers/merge_requests/assignees_change_worker.rb4
-rw-r--r--app/workers/merge_requests/create_pipeline_worker.rb4
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb4
-rw-r--r--app/workers/merge_requests/handle_assignees_change_worker.rb4
-rw-r--r--app/workers/merge_requests/resolve_todos_worker.rb2
-rw-r--r--app/workers/merge_worker.rb16
-rw-r--r--app/workers/metrics/dashboard/prune_old_annotations_worker.rb2
-rw-r--r--app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb2
-rw-r--r--app/workers/metrics/dashboard/sync_dashboards_worker.rb3
-rw-r--r--app/workers/migrate_external_diffs_worker.rb2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb2
-rw-r--r--app/workers/namespaces/in_product_marketing_emails_worker.rb5
-rw-r--r--app/workers/namespaces/onboarding_issue_created_worker.rb3
-rw-r--r--app/workers/namespaces/onboarding_pipeline_created_worker.rb3
-rw-r--r--app/workers/namespaces/onboarding_progress_worker.rb3
-rw-r--r--app/workers/namespaces/onboarding_user_added_worker.rb3
-rw-r--r--app/workers/namespaces/prune_aggregation_schedules_worker.rb2
-rw-r--r--app/workers/namespaces/root_statistics_worker.rb2
-rw-r--r--app/workers/namespaces/schedule_aggregation_worker.rb2
-rw-r--r--app/workers/new_issue_worker.rb4
-rw-r--r--app/workers/new_merge_request_worker.rb4
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_pool/create_worker.rb4
-rw-r--r--app/workers/object_pool/destroy_worker.rb2
-rw-r--r--app/workers/object_pool/join_worker.rb2
-rw-r--r--app/workers/object_pool/schedule_join_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb6
-rw-r--r--app/workers/packages/composer/cache_cleanup_worker.rb5
-rw-r--r--app/workers/packages/composer/cache_update_worker.rb5
-rw-r--r--app/workers/packages/debian/process_changes_worker.rb50
-rw-r--r--app/workers/packages/go/sync_packages_worker.rb3
-rw-r--r--app/workers/packages/maven/metadata/sync_worker.rb7
-rw-r--r--app/workers/packages/nuget/extraction_worker.rb7
-rw-r--r--app/workers/packages/rubygems/extraction_worker.rb9
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb4
-rw-r--r--app/workers/pages_domain_ssl_renewal_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_ssl_renewal_worker.rb4
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_worker.rb4
-rw-r--r--app/workers/pages_remove_worker.rb1
-rw-r--r--app/workers/pages_transfer_worker.rb3
-rw-r--r--app/workers/pages_update_configuration_worker.rb3
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/partition_creation_worker.rb2
-rw-r--r--app/workers/personal_access_tokens/expired_notification_worker.rb3
-rw-r--r--app/workers/personal_access_tokens/expiring_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/pipeline_notification_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb3
-rw-r--r--app/workers/pipeline_schedule_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/post_receive.rb6
-rw-r--r--app/workers/process_commit_worker.rb4
-rw-r--r--app/workers/project_cache_worker.rb4
-rw-r--r--app/workers/project_daily_statistics_worker.rb2
-rw-r--r--app/workers/project_destroy_worker.rb4
-rw-r--r--app/workers/project_service_worker.rb12
-rw-r--r--app/workers/projects/git_garbage_collect_worker.rb4
-rw-r--r--app/workers/projects/post_creation_worker.rb3
-rw-r--r--app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb2
-rw-r--r--app/workers/projects/update_repository_storage_worker.rb2
-rw-r--r--app/workers/prometheus/create_default_alerts_worker.rb2
-rw-r--r--app/workers/propagate_integration_group_worker.rb7
-rw-r--r--app/workers/propagate_integration_inherit_descendant_worker.rb7
-rw-r--r--app/workers/propagate_integration_inherit_worker.rb7
-rw-r--r--app/workers/propagate_integration_project_worker.rb7
-rw-r--r--app/workers/propagate_integration_worker.rb4
-rw-r--r--app/workers/propagate_service_template_worker.rb4
-rw-r--r--app/workers/prune_old_events_worker.rb2
-rw-r--r--app/workers/prune_web_hook_logs_worker.rb2
-rw-r--r--app/workers/purge_dependency_proxy_cache_worker.rb2
-rw-r--r--app/workers/rebase_worker.rb4
-rw-r--r--app/workers/releases/create_evidence_worker.rb3
-rw-r--r--app/workers/releases/manage_evidence_worker.rb3
-rw-r--r--app/workers/remote_mirror_notification_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_expired_members_worker.rb4
-rw-r--r--app/workers/remove_unaccepted_member_invites_worker.rb3
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb2
-rw-r--r--app/workers/repository_archive_cache_worker.rb2
-rw-r--r--app/workers/repository_check/batch_worker.rb4
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/dispatch_worker.rb2
-rw-r--r--app/workers/repository_check/single_repository_worker.rb4
-rw-r--r--app/workers/repository_fork_worker.rb2
-rw-r--r--app/workers/repository_import_worker.rb2
-rw-r--r--app/workers/repository_remove_remote_worker.rb2
-rw-r--r--app/workers/requests_profiles_worker.rb2
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb4
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb3
-rw-r--r--app/workers/schedule_migrate_external_diffs_worker.rb2
-rw-r--r--app/workers/self_monitoring_project_create_worker.rb2
-rw-r--r--app/workers/self_monitoring_project_delete_worker.rb2
-rw-r--r--app/workers/service_desk_email_receiver_worker.rb17
-rw-r--r--app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb2
-rw-r--r--app/workers/snippets/update_repository_storage_worker.rb2
-rw-r--r--app/workers/ssh_keys/expired_notification_worker.rb7
-rw-r--r--app/workers/ssh_keys/expiring_soon_notification_worker.rb7
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb4
-rw-r--r--app/workers/stuck_export_jobs_worker.rb2
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb2
-rw-r--r--app/workers/system_hook_push_worker.rb2
-rw-r--r--app/workers/todos_destroyer/confidential_issue_worker.rb2
-rw-r--r--app/workers/todos_destroyer/destroyed_issuable_worker.rb4
-rw-r--r--app/workers/todos_destroyer/entity_leave_worker.rb2
-rw-r--r--app/workers/todos_destroyer/group_private_worker.rb2
-rw-r--r--app/workers/todos_destroyer/private_features_worker.rb2
-rw-r--r--app/workers/todos_destroyer/project_private_worker.rb2
-rw-r--r--app/workers/trending_projects_worker.rb2
-rw-r--r--app/workers/update_container_registry_info_worker.rb2
-rw-r--r--app/workers/update_external_pull_requests_worker.rb2
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
-rw-r--r--app/workers/update_highest_role_worker.rb4
-rw-r--r--app/workers/update_merge_requests_worker.rb4
-rw-r--r--app/workers/update_project_statistics_worker.rb2
-rw-r--r--app/workers/upload_checksum_worker.rb2
-rw-r--r--app/workers/user_status_cleanup/batch_worker.rb3
-rw-r--r--app/workers/users/create_statistics_worker.rb2
-rw-r--r--app/workers/users/deactivate_dormant_users_worker.rb51
-rw-r--r--app/workers/users/update_open_issue_count_worker.rb26
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb2
-rw-r--r--app/workers/web_hook_worker.rb5
-rw-r--r--app/workers/web_hooks/destroy_worker.rb3
-rw-r--r--app/workers/wikis/git_garbage_collect_worker.rb2
-rw-r--r--app/workers/x509_certificate_revoke_worker.rb2
-rw-r--r--app/workers/x509_issuer_crl_check_worker.rb2
2375 files changed, 27116 insertions, 15004 deletions
diff --git a/app/views/shared/icons/_dev_ops_report_no_data.svg b/app/assets/images/dev_ops_report_no_data.svg
index 5de929859ae..5de929859ae 100644
--- a/app/views/shared/icons/_dev_ops_report_no_data.svg
+++ b/app/assets/images/dev_ops_report_no_data.svg
diff --git a/app/assets/images/learn_gitlab/get_started.svg b/app/assets/images/learn_gitlab/get_started.svg
new file mode 100644
index 00000000000..0e682842b1f
--- /dev/null
+++ b/app/assets/images/learn_gitlab/get_started.svg
@@ -0,0 +1 @@
+<svg width="468" height="96" xmlns="http://www.w3.org/2000/svg"><g transform="translate(4)" fill="none" fill-rule="evenodd"><path d="M19.817 6.79c.398-1.258 1.516-2.107 2.776-2.107h1.634c1.26 0 2.378.849 2.776 2.107l1.537 4.858c1.088.35 2.13.807 3.117 1.36l4.35-2.29c1.127-.593 2.487-.36 3.378.577l1.156 1.217c.89.938 1.111 2.37.548 3.557l-2.175 4.582c.525 1.038.96 2.136 1.291 3.281l4.613 1.62c1.195.419 2.001 1.597 2.001 2.923v1.721c0 1.326-.806 2.504-2 2.923l-4.614 1.62a18.947 18.947 0 01-1.291 3.281l2.175 4.582c.563 1.186.342 2.619-.548 3.557l-1.156 1.217c-.89.938-2.251 1.17-3.378.577l-4.35-2.29a16.947 16.947 0 01-3.117 1.36l-1.537 4.858c-.398 1.258-1.517 2.107-2.776 2.107h-1.634c-1.26 0-2.378-.849-2.776-2.107l-1.538-4.858a16.973 16.973 0 01-3.116-1.36l-4.35 2.29c-1.127.593-2.488.36-3.379-.577L6.28 46.159c-.89-.938-1.112-2.37-.548-3.557l2.175-4.582a18.93 18.93 0 01-1.292-3.281l-4.613-1.62C.806 32.7 0 31.522 0 30.196v-1.721c0-1.326.806-2.504 2-2.923l4.614-1.62a18.932 18.932 0 011.292-3.281L5.73 16.069c-.564-1.186-.343-2.62.548-3.557l1.155-1.217c.89-.938 2.252-1.17 3.378-.577l4.35 2.29a16.975 16.975 0 013.117-1.36l1.538-4.858zm3.593 34.872c6.464 0 11.705-5.52 11.705-12.327S29.874 17.01 23.41 17.01c-6.465 0-11.705 5.519-11.705 12.326 0 6.808 5.24 12.327 11.705 12.327z" stroke="#6E49CB" stroke-width="2" fill="#EFEDF8"/><path d="M23.41 37.039c4.04 0 7.315-3.45 7.315-7.704 0-4.255-3.275-7.704-7.315-7.704-4.04 0-7.316 3.45-7.316 7.704 0 4.255 3.276 7.704 7.316 7.704z" stroke="#6E49CB" stroke-linecap="round"/><path d="M218.894 5.854c1.177 0 2.186.207 2.858.497.337.145.536.288.633.388a.555.555 0 01.034.037v44.362c-.008.01-.019.023-.034.038-.097.099-.296.242-.633.388-.672.289-1.681.497-2.858.497-1.176 0-2.186-.208-2.857-.497-.338-.146-.537-.289-.634-.388-.015-.015-.026-.028-.034-.038V6.776a.554.554 0 01.034-.037c.097-.1.296-.243.634-.388.671-.29 1.681-.497 2.857-.497z" stroke="#6E49CB" stroke-width="2" fill="#EFEDF8" fill-rule="nonzero"/><path d="M223.59 6.665l30.511 9.4c2.93.902 2.983 2.492.135 3.545l-30.647 11.33V6.664z" fill="#6E49CB"/><path d="M434.38 42.624h-.119l-.11.046c-2.675 1.14-5.224 1.423-7.758.61l-.283-.09-.241.176a4.655 4.655 0 01-6.045-.465v0l-3.178-3.179a4.659 4.659 0 01-.159-6.422l.206-.227-.069-.298c-.496-2.14-.302-4.32.467-6.575l.183-.533-.526-.202-4.709-1.818a3.605 3.605 0 01-2.195-2.467 3.622 3.622 0 01.73-3.221s0 0 0 0l6.166-7.35v0a3.612 3.612 0 014.064-1.047h.001l6.025 2.326.361.14.27-.28c.577-.594 1.167-1.193 1.772-1.798 5.89-5.893 13.552-9.022 23.03-9.36.258-.01.515-.004.773.016v0a6.906 6.906 0 016.359 7.412c-.692 9.033-3.963 16.452-9.804 22.292v0c-.413.414-.826.826-1.242 1.237l-.272.269.137.357 2.433 6.302s0 0 0 0a3.6 3.6 0 01.127 2.2 3.603 3.603 0 01-1.175 1.865h0l-7.347 6.168.376.448-.376-.448a3.613 3.613 0 01-3.221.73 3.614 3.614 0 01-2.466-2.196v0l-1.638-4.244-.144-.374h-.403zm-17.378-19.038l.391.15.268-.32 7.795-9.291.526-.628-.764-.295-5.11-1.972v0a1.785 1.785 0 00-2.008.517v0l-6.166 7.35v0a1.789 1.789 0 00-.36 1.592 1.78 1.78 0 001.085 1.22v0l4.343 1.677zm19.584 18.482v.109l.04.102 1.676 4.345v0c.103.265.266.501.478.69l.389-.437-.389.437a1.78 1.78 0 002.332.034l7.348-6.167v0c.285-.24.488-.561.58-.922l-.566-.146.567.146c.093-.36.071-.74-.062-1.087h-.001l-1.972-5.111-.294-.764-.628.526-9.289 7.796-.21.175v.274zm17.83-39.285l-.22.542.22-.542a5.079 5.079 0 00-2.089-.369h0c-8.997.321-16.228 3.251-21.803 8.827-6.376 6.377-10.292 11.317-11.722 15.569-.723 2.148-.817 4.138-.263 6.047.553 1.901 1.732 3.67 3.47 5.41 1.676 1.675 3.328 2.828 5.103 3.357 1.788.532 3.639.412 5.67-.351 4.005-1.504 8.816-5.55 15.517-12.253 5.54-5.54 8.618-12.556 9.273-21.141a5.076 5.076 0 00-3.156-5.096z" stroke="#6E49CB" fill="#6E49CB" fill-rule="nonzero"/><path d="M440.596 20.006a2.998 2.998 0 004.26.023 2.99 2.99 0 00.877-2.135 3.003 3.003 0 00-.901-2.126 2.996 2.996 0 10-4.237 4.238zm-2.12 2.12a6.001 6.001 0 01-1.3-6.532 5.98 5.98 0 013.244-3.245 5.99 5.99 0 016.53 1.3 5.992 5.992 0 01-8.473 8.475l-.001.001z" fill="#C2B7E6" fill-rule="nonzero"/><path d="M412.525 39.607a1.498 1.498 0 010 2.118l-8.473 8.475a1.502 1.502 0 01-2.137.02 1.513 1.513 0 01-.328-.493 1.49 1.49 0 01.345-1.645l8.475-8.476a1.501 1.501 0 011.632-.325c.183.075.348.186.486.325zm7.415 7.416a1.498 1.498 0 010 2.118l-6.356 6.357a1.501 1.501 0 01-1.631.325 1.495 1.495 0 01-.811-1.958c.075-.181.185-.347.324-.486l6.356-6.356a1.5 1.5 0 012.118 0z" fill="#E0DBF2"/><path d="M416.232 43.314a1.498 1.498 0 010 2.12l-11.65 11.654a1.497 1.497 0 01-2.558-1.06c0-.398.158-.779.439-1.06l11.652-11.653a1.497 1.497 0 012.117 0z" fill="#C2B7E6"/><path d="M7.468 84.21h1.474c-.022-.644-.147-1.196-.377-1.655a3.111 3.111 0 00-.917-1.163 3.704 3.704 0 00-1.344-.672 6.253 6.253 0 00-1.671-.213c-.536 0-1.06.07-1.573.213a4.175 4.175 0 00-1.36.622c-.394.274-.71.629-.951 1.066-.24.426-.36.934-.36 1.524 0 .535.103.984.31 1.344.22.35.503.64.853.869.36.218.765.399 1.213.54.447.132.9.252 1.36.361.47.099.928.197 1.376.295.448.099.847.23 1.196.394.36.153.645.355.853.606.218.251.327.58.327.983 0 .427-.087.776-.262 1.05a1.98 1.98 0 01-.688.655 3.252 3.252 0 01-.967.328c-.35.065-.7.098-1.049.098-.437 0-.863-.054-1.278-.164a3.269 3.269 0 01-1.098-.508 2.762 2.762 0 01-.754-.868c-.185-.361-.278-.787-.278-1.279H.028c0 .71.126 1.328.377 1.852.262.514.612.94 1.049 1.279a4.79 4.79 0 001.54.737 6.948 6.948 0 003.474.05 4.641 4.641 0 001.475-.59 3.472 3.472 0 001.065-1.082c.284-.448.426-.984.426-1.607 0-.579-.11-1.06-.328-1.442a2.736 2.736 0 00-.852-.95 4.215 4.215 0 00-1.196-.59 14.217 14.217 0 00-1.377-.394c-.458-.11-.912-.208-1.36-.295a7.987 7.987 0 01-1.212-.36 2.535 2.535 0 01-.852-.542c-.208-.229-.312-.524-.312-.885 0-.382.071-.699.213-.95.153-.263.35-.47.59-.623a2.66 2.66 0 01.852-.328c.317-.065.64-.098.967-.098.808 0 1.47.19 1.983.573.524.372.83.978.918 1.82zm9.511 3.23h-4.867a2.82 2.82 0 01.213-.918c.12-.295.284-.552.492-.77.207-.22.453-.388.737-.509.295-.13.623-.196.983-.196.35 0 .667.065.95.196.296.12.547.29.755.508.218.208.387.46.508.754.13.295.207.607.229.935zm1.344 2.36h-1.377c-.12.557-.371.972-.754 1.245-.37.274-.852.41-1.442.41-.458 0-.857-.076-1.196-.23a2.394 2.394 0 01-.836-.606 2.483 2.483 0 01-.475-.885 3.438 3.438 0 01-.13-1.065h6.34a6.499 6.499 0 00-.147-1.623 4.48 4.48 0 00-.622-1.573 3.533 3.533 0 00-1.197-1.18c-.491-.317-1.114-.476-1.868-.476-.579 0-1.114.11-1.606.328-.48.219-.9.525-1.261.918-.35.393-.623.858-.82 1.393a5.1 5.1 0 00-.295 1.77c.022.645.115 1.24.279 1.787.175.546.431 1.016.77 1.41.339.393.754.699 1.246.917.502.219 1.092.328 1.77.328.96 0 1.758-.24 2.392-.721s1.043-1.197 1.229-2.147zm3.508-5.786v-2.54h-1.393v2.54h-1.442v1.23h1.442v5.392c0 .393.039.71.115.95.077.24.191.426.344.558.164.13.372.224.623.278.262.044.573.066.934.066h1.065v-1.23h-.639a7.31 7.31 0 01-.54-.016.742.742 0 01-.312-.115.446.446 0 01-.164-.229 1.883 1.883 0 01-.033-.393v-5.262h1.688v-1.229h-1.688zm14.81 8.474v-8.474h-1.393V88.8c0 .382-.055.738-.164 1.065a2.31 2.31 0 01-.459.836c-.207.24-.47.426-.786.558-.306.13-.672.196-1.098.196-.535 0-.956-.153-1.262-.459-.306-.306-.459-.72-.459-1.245v-5.737h-1.393v5.573c0 .459.044.88.131 1.262.099.371.263.694.492.967.23.273.53.486.901.639.372.142.836.213 1.393.213.623 0 1.164-.12 1.622-.36.46-.252.836-.64 1.131-1.164h.033v1.344h1.31zm2.141-8.474v11.703h1.393v-4.36h.033c.153.251.338.464.557.64.23.163.47.294.72.392.252.099.503.17.755.214.262.043.497.065.704.065.645 0 1.207-.115 1.688-.344.492-.23.896-.541 1.213-.934a3.92 3.92 0 00.72-1.41c.165-.535.246-1.104.246-1.704s-.081-1.17-.245-1.705a4.132 4.132 0 00-.738-1.41 3.35 3.35 0 00-1.212-.983c-.481-.24-1.05-.36-1.705-.36-.59 0-1.13.109-1.622.327a2.05 2.05 0 00-1.081 1.016h-.033v-1.147h-1.393zm6.555 4.163c0 .415-.044.82-.131 1.213a3.21 3.21 0 01-.426 1.049 2.22 2.22 0 01-.787.738c-.317.185-.716.278-1.196.278-.481 0-.89-.087-1.23-.262a2.55 2.55 0 01-.835-.721 3.082 3.082 0 01-.459-1.016 4.983 4.983 0 01-.147-1.213c0-.394.043-.781.13-1.164.1-.382.252-.721.46-1.016.207-.306.475-.552.803-.738.327-.186.726-.278 1.196-.278.448 0 .836.087 1.163.262.339.175.612.41.82.705a2.9 2.9 0 01.475 1.016c.109.371.164.754.164 1.147zm129.786-1.786v-4.295h3.048c.885 0 1.53.186 1.934.558.415.36.622.89.622 1.59 0 .699-.207 1.234-.622 1.606-.404.371-1.05.552-1.934.54h-3.048zm-1.557-5.606v11.703h1.557v-4.786h3.572c1.18.01 2.071-.29 2.671-.902.613-.612.918-1.464.918-2.556 0-1.093-.305-1.94-.918-2.541-.6-.612-1.49-.918-2.67-.918h-5.13zm10.478 0v11.703h1.393V80.785h-1.393zm11.057 11.67c-.24.142-.574.213-1 .213-.36 0-.65-.098-.868-.295-.207-.207-.312-.54-.312-1-.382.46-.83.793-1.343 1a4.456 4.456 0 01-1.639.295c-.383 0-.748-.044-1.098-.131a2.545 2.545 0 01-.885-.41 2.06 2.06 0 01-.606-.721c-.142-.306-.213-.672-.213-1.098 0-.48.082-.874.246-1.18.164-.306.377-.552.639-.738.273-.196.58-.344.917-.442.35-.099.705-.18 1.066-.246.382-.077.743-.131 1.081-.164.35-.044.656-.098.918-.164.262-.076.47-.18.622-.311.154-.142.23-.345.23-.607 0-.306-.06-.552-.18-.737a1.1 1.1 0 00-.443-.427 1.72 1.72 0 00-.606-.196 4.412 4.412 0 00-.656-.05c-.59 0-1.081.115-1.474.345-.394.218-.607.639-.64 1.262h-1.392c.022-.525.13-.967.327-1.328.197-.36.46-.65.787-.868a3.21 3.21 0 011.114-.492 6.032 6.032 0 011.36-.148c.383 0 .76.028 1.131.082.383.055.727.17 1.032.345.306.163.552.398.738.704.186.306.278.705.278 1.197v4.36c0 .327.017.568.05.72.043.154.174.23.393.23.12 0 .262-.027.426-.082v1.082zm-2.262-4.343c-.174.13-.403.23-.688.295-.284.054-.584.103-.901.147a14.79 14.79 0 00-.934.131c-.317.044-.6.12-.852.23a1.58 1.58 0 00-.623.475c-.153.197-.23.47-.23.82 0 .229.044.426.132.59.098.153.219.278.36.377.154.098.328.169.525.213.196.043.404.065.622.065.46 0 .852-.06 1.18-.18.328-.131.596-.29.803-.475.207-.197.36-.405.459-.623.098-.23.147-.443.147-.64v-1.425zm3.559-4.098v8.474h1.393v-4.786c0-.383.049-.732.147-1.05.109-.327.267-.611.475-.851.208-.24.464-.427.77-.558a2.898 2.898 0 011.115-.196c.535 0 .955.153 1.262.459.305.305.458.72.458 1.245v5.737h1.393v-5.573c0-.459-.049-.874-.147-1.246a2.302 2.302 0 00-.475-.983 2.294 2.294 0 00-.902-.64c-.372-.152-.835-.229-1.393-.229-1.257 0-2.174.514-2.753 1.541h-.032v-1.344h-1.311zm17.123 1.278l-.394-.491a7.294 7.294 0 01-.36-.492 4.216 4.216 0 01-.279-.524 1.485 1.485 0 01-.098-.525c0-.426.143-.737.426-.934.285-.208.59-.312.918-.312.415 0 .743.126.983.377.241.24.36.53.36.87 0 .25-.049.48-.147.687a2.28 2.28 0 01-.377.541 3.227 3.227 0 01-.508.443c-.185.131-.36.251-.524.36zm2.999 5.737l1.245 1.459h1.819l-2.278-2.639c.12-.262.225-.492.311-.688.088-.208.16-.416.213-.623.066-.208.115-.432.148-.672.044-.252.082-.552.115-.902h-1.328a6.424 6.424 0 01-.377 1.82l-2.113-2.574c.284-.164.557-.344.819-.54a4.65 4.65 0 00.705-.689c.208-.251.372-.524.491-.82.12-.305.18-.633.18-.983 0-.393-.082-.737-.245-1.032a2.093 2.093 0 00-.623-.754 2.587 2.587 0 00-.918-.46 3.74 3.74 0 00-1.049-.147c-.426 0-.802.066-1.13.197a2.327 2.327 0 00-1.344 1.311 2.498 2.498 0 00-.164.902c0 .295.033.562.099.803.077.23.175.453.294.672.132.207.274.415.427.622.153.208.31.427.475.656a11.6 11.6 0 00-1.065.64c-.328.218-.617.47-.869.753-.24.273-.436.585-.59.935-.141.35-.213.753-.213 1.212 0 .24.039.541.115.902.088.36.262.71.524 1.049.262.339.635.628 1.115.869.48.24 1.114.36 1.9.36.645 0 1.268-.137 1.869-.41a3.23 3.23 0 001.442-1.229zm-3.458-4.196l2.687 3.229c-.283.426-.639.765-1.065 1.016-.414.252-.89.377-1.425.377-.284 0-.562-.049-.836-.147a2.615 2.615 0 01-.705-.394 2.164 2.164 0 01-.508-.622 1.886 1.886 0 01-.18-.82c0-.35.055-.65.164-.901.11-.263.257-.498.442-.705.186-.208.4-.394.64-.558.251-.163.513-.322.786-.475zm17.767.607h-4.867a2.82 2.82 0 01.213-.918 2.46 2.46 0 01.491-.77c.208-.22.453-.388.738-.509.295-.13.623-.196.983-.196.35 0 .666.065.95.196.295.12.547.29.754.508.22.208.388.46.508.754.131.295.207.607.23.935zm1.343 2.36h-1.376c-.12.557-.371.972-.754 1.245-.37.274-.852.41-1.442.41-.459 0-.858-.076-1.196-.23a2.4 2.4 0 01-.836-.606 2.477 2.477 0 01-.475-.885 3.448 3.448 0 01-.131-1.065h6.342a6.466 6.466 0 00-.148-1.623 4.48 4.48 0 00-.623-1.573 3.536 3.536 0 00-1.196-1.18c-.492-.317-1.114-.476-1.868-.476-.58 0-1.114.11-1.606.328a3.863 3.863 0 00-1.262.918c-.35.393-.622.858-.819 1.393a5.1 5.1 0 00-.295 1.77c.022.645.115 1.24.279 1.787.174.546.432 1.016.77 1.41.338.393.754.699 1.245.917.502.219 1.092.328 1.77.328.96 0 1.76-.24 2.392-.721.634-.48 1.043-1.197 1.23-2.147zm3.854-1.77l-3.18 4.458h1.688l2.36-3.508 2.36 3.508h1.786l-3.277-4.573 2.916-3.901h-1.671l-2.114 2.967-2.032-2.967h-1.786l2.95 4.016zm12.086-.59h-4.867a2.79 2.79 0 01.213-.918c.12-.295.283-.552.492-.77.207-.22.453-.388.737-.509.295-.13.623-.196.983-.196.35 0 .666.065.95.196.296.12.546.29.755.508.217.208.387.46.508.754.13.295.207.607.23.935zm1.344 2.36h-1.377c-.12.557-.372.972-.754 1.245-.372.274-.852.41-1.442.41-.458 0-.858-.076-1.196-.23a2.379 2.379 0 01-.835-.606 2.477 2.477 0 01-.476-.885 3.413 3.413 0 01-.13-1.065h6.34a6.577 6.577 0 00-.147-1.623 4.503 4.503 0 00-.622-1.573 3.536 3.536 0 00-1.197-1.18c-.491-.317-1.114-.476-1.868-.476-.579 0-1.114.11-1.606.328a3.863 3.863 0 00-1.261.918c-.35.393-.623.858-.82 1.393a5.1 5.1 0 00-.295 1.77c.021.645.115 1.24.279 1.787a3.98 3.98 0 00.77 1.41c.338.393.754.699 1.246.917.502.219 1.092.328 1.77.328.96 0 1.757-.24 2.392-.721.633-.48 1.043-1.197 1.229-2.147zm7.326-3.065h1.442c-.054-.503-.185-.934-.393-1.295a2.832 2.832 0 00-.803-.918 3.084 3.084 0 00-1.115-.524 4.806 4.806 0 00-1.343-.18c-.666 0-1.25.12-1.754.36-.502.23-.922.552-1.262.967a4.095 4.095 0 00-.737 1.442 6.135 6.135 0 00-.246 1.77c0 .634.082 1.219.246 1.754.176.525.426.978.754 1.36.34.383.754.678 1.245.886.504.207 1.077.311 1.72.311 1.082 0 1.934-.284 2.557-.852.635-.568 1.028-1.377 1.18-2.426h-1.426c-.086.656-.327 1.164-.72 1.524-.382.36-.918.541-1.607.541-.436 0-.813-.087-1.13-.262a2.238 2.238 0 01-.77-.688 3.318 3.318 0 01-.443-1 5.037 5.037 0 01-.13-1.148c0-.426.044-.835.13-1.229.088-.404.23-.76.426-1.065.209-.306.481-.552.82-.738.339-.186.76-.278 1.261-.278.59 0 1.06.147 1.41.442.35.295.579.71.688 1.246zm10.064 5.753v-8.474h-1.393V88.8c0 .382-.055.738-.164 1.065a2.316 2.316 0 01-.459.836c-.208.24-.47.426-.786.558-.307.13-.672.196-1.098.196-.536 0-.957-.153-1.262-.459-.307-.306-.459-.72-.459-1.245v-5.737h-1.393v5.573c0 .459.043.88.131 1.262.099.371.262.694.492.967.23.273.529.486.901.639.371.142.836.213 1.393.213.623 0 1.163-.12 1.622-.36.46-.252.836-.64 1.13-1.164h.034v1.344h1.31zm4.025-8.474v-2.54h-1.393v2.54h-1.442v1.23h1.442v5.392c0 .393.039.71.115.95.076.24.19.426.344.558.164.13.37.224.623.278.262.044.573.066.934.066h1.065v-1.23h-.64c-.218 0-.398-.005-.54-.016a.74.74 0 01-.311-.115.448.448 0 01-.164-.229 1.86 1.86 0 01-.033-.393v-5.262h1.688v-1.229h-1.688zm9.118 3.426h-4.867a2.82 2.82 0 01.213-.918 2.46 2.46 0 01.492-.77c.207-.22.453-.388.737-.509.295-.13.623-.196.983-.196.35 0 .666.065.95.196.296.12.547.29.755.508.218.208.387.46.508.754.13.295.207.607.229.935zm1.344 2.36h-1.377c-.12.557-.37.972-.754 1.245-.37.274-.852.41-1.442.41-.458 0-.858-.076-1.196-.23a2.4 2.4 0 01-.836-.606 2.477 2.477 0 01-.475-.885 3.448 3.448 0 01-.13-1.065h6.34a6.466 6.466 0 00-.147-1.623 4.48 4.48 0 00-.622-1.573 3.536 3.536 0 00-1.197-1.18c-.491-.317-1.114-.476-1.868-.476-.58 0-1.114.11-1.606.328a3.863 3.863 0 00-1.261.918c-.35.393-.623.858-.82 1.393a5.1 5.1 0 00-.295 1.77 7.05 7.05 0 00.279 1.787c.174.546.432 1.016.77 1.41.338.393.754.699 1.245.917.503.219 1.093.328 1.77.328.961 0 1.76-.24 2.393-.721s1.043-1.197 1.229-2.147zm126.596 1.377v-9.08h2.622c.72 0 1.327.103 1.819.31.491.197.89.493 1.196.886.316.382.54.852.672 1.41.141.546.213 1.169.213 1.868 0 .721-.078 1.338-.23 1.852-.143.503-.328.923-.557 1.262-.23.339-.492.607-.786.803-.285.197-.574.35-.869.46a4.367 4.367 0 01-.836.196 8.132 8.132 0 01-.655.033h-2.59zm-1.557-10.392v11.703h4.015c.971 0 1.813-.137 2.523-.41.71-.273 1.295-.666 1.754-1.18.458-.525.797-1.164 1.016-1.918.217-.765.327-1.639.327-2.622 0-1.88-.487-3.278-1.458-4.196-.973-.918-2.36-1.377-4.162-1.377h-4.015zm17.19 6.655h-4.866a2.85 2.85 0 01.213-.918 2.46 2.46 0 01.492-.77c.208-.22.454-.388.737-.509.295-.13.623-.196.983-.196.35 0 .667.065.95.196.296.12.547.29.754.508.22.208.389.46.508.754.132.295.209.607.23.935zm1.345 2.36h-1.377c-.12.557-.371.972-.754 1.245-.37.274-.852.41-1.442.41-.459 0-.857-.076-1.196-.23a2.4 2.4 0 01-.836-.606 2.495 2.495 0 01-.475-.885 3.448 3.448 0 01-.131-1.065h6.342a6.466 6.466 0 00-.148-1.623 4.458 4.458 0 00-.623-1.573 3.526 3.526 0 00-1.196-1.18c-.491-.317-1.114-.476-1.868-.476a3.91 3.91 0 00-1.606.328c-.48.219-.901.525-1.262.918a4.322 4.322 0 00-.819 1.393 5.1 5.1 0 00-.295 1.77 7.05 7.05 0 00.279 1.787c.175.546.432 1.016.77 1.41.34.393.754.699 1.245.917.504.219 1.094.328 1.77.328.962 0 1.76-.24 2.393-.721.634-.48 1.044-1.197 1.229-2.147zm1.624-5.786v11.703h1.393v-4.36h.033a2.4 2.4 0 00.557.64c.23.163.47.294.721.392.25.099.502.17.754.214.262.043.496.065.704.065.644 0 1.207-.115 1.688-.344.492-.23.896-.541 1.213-.934.328-.405.568-.875.72-1.41.165-.535.247-1.104.247-1.704s-.082-1.17-.246-1.705a4.132 4.132 0 00-.737-1.41 3.353 3.353 0 00-1.213-.983c-.481-.24-1.049-.36-1.704-.36-.59 0-1.131.109-1.623.327a2.05 2.05 0 00-1.081 1.016h-.033v-1.147h-1.393zm6.555 4.163c0 .415-.045.82-.131 1.213-.088.393-.23.743-.426 1.049a2.219 2.219 0 01-.787.738c-.317.185-.716.278-1.196.278-.481 0-.89-.087-1.23-.262a2.534 2.534 0 01-.835-.721 3.065 3.065 0 01-.459-1.016 5.185 5.185 0 01-.016-2.377c.098-.382.25-.721.459-1.016.207-.306.475-.552.803-.738.328-.186.725-.278 1.196-.278.447 0 .836.087 1.163.262.339.175.611.41.82.705.218.295.377.633.475 1.016.109.371.164.754.164 1.147zm3.192-7.392v11.703h1.393V80.785h-1.393zm4.566 7.474a4.3 4.3 0 01.197-1.36c.141-.405.333-.743.573-1.016.24-.274.519-.481.836-.623a2.57 2.57 0 012.048 0c.328.142.612.35.852.623s.426.611.557 1.016c.142.393.213.847.213 1.36s-.071.973-.213 1.377a2.868 2.868 0 01-.557 1c-.24.262-.524.464-.852.606a2.57 2.57 0 01-2.048 0 2.518 2.518 0 01-.836-.606 3.075 3.075 0 01-.573-1 4.456 4.456 0 01-.197-1.377zm-1.475 0c0 .623.088 1.202.262 1.737.175.536.437 1.006.787 1.41.35.393.78.705 1.294.934.514.219 1.104.328 1.77.328.678 0 1.268-.11 1.77-.328.514-.23.944-.54 1.294-.934.35-.404.613-.874.787-1.41a5.579 5.579 0 00.262-1.737c0-.623-.088-1.202-.262-1.737a3.852 3.852 0 00-.787-1.41 3.659 3.659 0 00-1.294-.95c-.502-.23-1.092-.345-1.77-.345-.666 0-1.256.115-1.77.345-.513.229-.944.546-1.294.95-.35.394-.612.863-.787 1.41a5.579 5.579 0 00-.262 1.737zm13.195 5.36a8.003 8.003 0 01-.492 1.049 2.468 2.468 0 01-.524.688 1.554 1.554 0 01-.64.393c-.229.088-.495.132-.802.132-.164 0-.328-.011-.492-.033a2.326 2.326 0 01-.475-.115v-1.278c.12.054.258.098.41.13.164.044.3.066.41.066.284 0 .52-.07.704-.213.197-.13.344-.322.442-.573l.574-1.426-3.36-8.425h1.574l2.474 6.933h.033l2.376-6.933h1.475l-3.687 9.605z" fill="#303030" fill-rule="nonzero"/><rect fill="#C2B7E6" fill-rule="nonzero" x="62.036" y="26.927" width="126.412" height="4.683" rx="2"/><rect fill="#C2B7E6" fill-rule="nonzero" x="266.87" y="26.927" width="125.242" height="4.683" rx="2"/></g></svg>
diff --git a/app/assets/images/learn_gitlab/graduation_hat.svg b/app/assets/images/learn_gitlab/graduation_hat.svg
new file mode 100644
index 00000000000..998d8d9b935
--- /dev/null
+++ b/app/assets/images/learn_gitlab/graduation_hat.svg
@@ -0,0 +1 @@
+<svg width="16" height="17" xmlns="http://www.w3.org/2000/svg"><path fill="#fffff" d="M1.53 7.639l-.476.88.476-.88zm0-1.758L1.054 5l.476.88zm2.257 2.982h1v-.596l-.523-.283-.477.879zm8.424 0l-.476-.88-.524.284v.596h1zm2.257-1.224l.477.88-.477-.88zm0-1.758l-.476.879.476-.88zM8.476 2.632l-.477.88.477-.88zm-.953 0l.476.88-.476-.88zM2.007 6.76l-.953-1.758c-1.396.756-1.396 2.76 0 3.516l.953-1.758zm2.257 1.224L2.007 6.76l-.953 1.758L3.31 9.742l.953-1.758zm.523 1.995V8.863h-2v1.116h2zM8 12.5c-1.949 0-3.212-1.289-3.212-2.52h-2c0 2.656 2.51 4.52 5.212 4.52v-2zm3.212-2.52c0 1.231-1.262 2.52-3.212 2.52v2c2.704 0 5.212-1.864 5.212-4.52h-2zm0-1.117v1.116h2V8.863h-2zm2.78-2.103l-2.256 1.223.953 1.759 2.257-1.224-.953-1.758zm0 0l.954 1.758c1.396-.757 1.396-2.76 0-3.516l-.953 1.758zM8 3.51l5.993 3.249.953-1.758-5.993-3.249L8 3.511zm0 0l.953-1.758a2 2 0 00-1.906 0L8 3.511zM2.007 6.76l5.992-3.25-.953-1.758-5.992 3.249.953 1.758z"/><path fill="#fffff" d="M7.228 7.541c-.187-.112-.277-.427-.201-.704.076-.276.288-.41.475-.297L11 8.644v5.316c0 .298-.163.54-.365.54-.2 0-.364-.242-.364-.54V9.37L7.228 7.54z"/></svg> \ No newline at end of file
diff --git a/app/assets/images/learn_gitlab/rectangle.svg b/app/assets/images/learn_gitlab/rectangle.svg
new file mode 100644
index 00000000000..51667e77158
--- /dev/null
+++ b/app/assets/images/learn_gitlab/rectangle.svg
@@ -0,0 +1 @@
+<svg width="108" height="4" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="108" height="4" rx="2" fill="#C2B7E6"/></svg>
diff --git a/app/assets/javascripts/actioncable_link.js b/app/assets/javascripts/actioncable_link.js
new file mode 100644
index 00000000000..895a34ba157
--- /dev/null
+++ b/app/assets/javascripts/actioncable_link.js
@@ -0,0 +1,40 @@
+import { ApolloLink, Observable } from 'apollo-link';
+import { print } from 'graphql';
+import cable from '~/actioncable_consumer';
+import { uuids } from '~/lib/utils/uuids';
+
+export default class ActionCableLink extends ApolloLink {
+ // eslint-disable-next-line class-methods-use-this
+ request(operation) {
+ return new Observable((observer) => {
+ const subscription = cable.subscriptions.create(
+ {
+ channel: 'GraphqlChannel',
+ query: operation.query ? print(operation.query) : null,
+ variables: operation.variables,
+ operationName: operation.operationName,
+ nonce: uuids()[0],
+ },
+ {
+ received(data) {
+ if (data.errors) {
+ observer.error(data.errors);
+ } else if (data.result) {
+ observer.next(data.result);
+ }
+
+ if (!data.more) {
+ observer.complete();
+ }
+ },
+ },
+ );
+
+ return {
+ unsubscribe() {
+ subscription.unsubscribe();
+ },
+ };
+ });
+ }
+}
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
index 725d3dbf388..6f4f272154a 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -14,12 +14,22 @@ export default {
type: Object,
required: true,
},
+ oncallSchedules: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
};
</script>
<template>
- <shared-delete-action modal-type="delete" :username="username" :paths="paths">
+ <shared-delete-action
+ modal-type="delete"
+ :username="username"
+ :paths="paths"
+ :oncall-schedules="oncallSchedules"
+ >
<slot></slot>
</shared-delete-action>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
index 0ae15bfbebb..82b09c04ab2 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
@@ -14,12 +14,22 @@ export default {
type: Object,
required: true,
},
+ oncallSchedules: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
};
</script>
<template>
- <shared-delete-action modal-type="delete-with-contributions" :username="username" :paths="paths">
+ <shared-delete-action
+ modal-type="delete-with-contributions"
+ :username="username"
+ :paths="paths"
+ :oncall-schedules="oncallSchedules"
+ >
<slot></slot>
</shared-delete-action>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
index 9107d9ccdd9..b3b68442e80 100644
--- a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
+++ b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
@@ -18,6 +18,10 @@ export default {
type: String,
required: true,
},
+ oncallSchedules: {
+ type: Array,
+ required: true,
+ },
},
computed: {
modalAttributes() {
@@ -26,6 +30,7 @@ export default {
'data-delete-user-url': this.paths.delete,
'data-gl-modal-action': this.modalType,
'data-username': this.username,
+ 'data-oncall-schedules': JSON.stringify(this.oncallSchedules),
};
},
},
diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue
index f2b501caf09..d4c0f900c94 100644
--- a/app/assets/javascripts/admin/users/components/actions/unblock.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue
@@ -23,9 +23,7 @@ export default {
'data-method': 'put',
'data-modal-attributes': JSON.stringify({
title: sprintf(s__('AdminUsers|Unblock user %{username}?'), { username: this.username }),
- message: s__(
- 'AdminUsers|You can always unblock their account, their data will remain intact.',
- ),
+ message: s__('AdminUsers|You can always block their account again if needed.'),
okVariant: 'confirm',
okTitle: s__('AdminUsers|Unblock'),
}),
diff --git a/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
deleted file mode 100644
index 5da38495010..00000000000
--- a/app/assets/javascripts/admin/users/components/usage_ping_disabled.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
-
-export default {
- components: {
- GlEmptyState,
- GlSprintf,
- GlLink,
- },
- inject: {
- svgPath: {
- default: '',
- },
- docsLink: {
- default: '',
- },
- primaryButtonPath: {
- default: '',
- },
- },
-};
-</script>
-<template>
- <gl-empty-state
- class="js-empty-state"
- :title="__('Activate user activity analysis')"
- :svg-path="svgPath"
- :primary-button-text="__('Turn on usage ping')"
- :primary-button-link="primaryButtonPath"
- >
- <template #description>
- <gl-sprintf
- :message="
- __(
- 'Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}.',
- )
- "
- >
- <template #docLink="{ content }">
- <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
- </template>
- <template #strong="{ content }"
- ><strong>{{ content }}</strong></template
- >
- </gl-sprintf>
- </template>
- </gl-empty-state>
-</template>
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index e92c97b54a3..b782526e6be 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -70,14 +70,14 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-justify-content-end">
+ <div class="gl-display-flex gl-justify-content-end" :data-testid="`user-actions-${user.id}`">
<gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
$options.i18n.edit
}}</gl-button>
<gl-dropdown
v-if="hasDropdownActions"
- data-testid="actions"
+ data-testid="dropdown-toggle"
right
class="gl-ml-2"
icon="settings"
@@ -109,6 +109,7 @@ export default {
:key="action"
:paths="userPaths"
:username="user.name"
+ :oncall-schedules="user.oncallSchedules"
:data-testid="`delete-${action}`"
>
{{ $options.i18n[action] }}
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index 8b41a063abc..2fd96e38f8e 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -1,7 +1,10 @@
<script>
-import { GlTable } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import { s__, __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
@@ -11,6 +14,7 @@ const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`;
export default {
components: {
+ GlSkeletonLoader,
GlTable,
UserAvatar,
UserActions,
@@ -26,6 +30,45 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: [],
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createFlash({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
fields: [
{
key: 'name',
@@ -38,6 +81,11 @@ export default {
thClass: thWidthClass(10),
},
{
+ key: 'groupCount',
+ label: __('Groups'),
+ thClass: thWidthClass(10),
+ },
+ {
key: 'createdAt',
label: __('Created on'),
thClass: thWidthClass(15),
@@ -50,7 +98,7 @@ export default {
{
key: 'settings',
label: '',
- thClass: thWidthClass(20),
+ thClass: thWidthClass(10),
},
],
};
@@ -64,6 +112,7 @@ export default {
:empty-text="s__('AdminUsers|No users found')"
show-empty
stacked="md"
+ data-qa-selector="user_row_content"
>
<template #cell(name)="{ item: user }">
<user-avatar :user="user" :admin-user-path="paths.adminUser" />
@@ -77,6 +126,17 @@ export default {
<user-date :date="lastActivityOn" show-never />
</template>
+ <template #cell(groupCount)="{ item: { id } }">
+ <div :data-testid="`user-group-count-${id}`">
+ <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] }}</span>
+ </div>
+ </template>
+
+ <template #cell(projectsCount)="{ item: { id, projectsCount } }">
+ <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
+ </template>
+
<template #cell(settings)="{ item: user }">
<user-actions :user="user" :paths="paths" />
</template>
diff --git a/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql
new file mode 100644
index 00000000000..0d8e199f16e
--- /dev/null
+++ b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql
@@ -0,0 +1,8 @@
+query getUsersGroupCounts($usernames: [String!]) {
+ users(usernames: $usernames) {
+ nodes {
+ id
+ groupCount
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 0365d054fc9..54c8edc080b 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -1,7 +1,14 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AdminUsersApp from './components/app.vue';
-import UsagePingDisabled from './components/usage_ping_disabled.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+});
export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
if (!el) {
@@ -12,6 +19,7 @@ export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-a
return new Vue({
el,
+ apolloProvider,
render: (createElement) =>
createElement(AdminUsersApp, {
props: {
@@ -21,23 +29,3 @@ export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-a
}),
});
};
-
-export const initCohortsEmptyState = (el = document.querySelector('#js-cohorts-empty-state')) => {
- if (!el) {
- return false;
- }
-
- const { emptyStateSvgPath, enableUsagePingLink, docsLink } = el.dataset;
-
- return new Vue({
- el,
- provide: {
- svgPath: emptyStateSvgPath,
- primaryButtonPath: enableUsagePingLink,
- docsLink,
- },
- render(h) {
- return h(UsagePingDisabled);
- },
- });
-};
diff --git a/app/assets/javascripts/admin/users/tabs.js b/app/assets/javascripts/admin/users/tabs.js
deleted file mode 100644
index cbaab7df4e9..00000000000
--- a/app/assets/javascripts/admin/users/tabs.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Api from '~/api';
-import { historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-
-const COHORTS_PANE = 'cohorts';
-const COHORTS_PANE_TAB_CLICK_EVENT = 'i_analytics_cohorts';
-
-const tabClickHandler = (e) => {
- const { hash } = e.currentTarget;
-
- let tab = null;
-
- if (hash === `#${COHORTS_PANE}`) {
- tab = COHORTS_PANE;
- Api.trackRedisHllUserEvent(COHORTS_PANE_TAB_CLICK_EVENT);
- }
-
- const newUrl = mergeUrlParams({ tab }, window.location.href);
- historyPushState(newUrl);
-};
-
-const initTabs = () => {
- const tabLinks = document.querySelectorAll('.js-users-tab-item a');
-
- if (tabLinks.length) {
- tabLinks.forEach((tabLink) => {
- tabLink.addEventListener('click', (e) => tabClickHandler(e));
- });
- }
-};
-
-export default initTabs;
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 79a6bac3ba7..8ea977698e1 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -17,6 +17,7 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
+import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import {
tdClass,
thClass,
@@ -96,6 +97,7 @@ export default {
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
+ AlertsDeprecationWarning,
GlAlert,
GlLoadingIcon,
GlTable,
@@ -273,6 +275,8 @@ export default {
</gl-sprintf>
</gl-alert>
+ <alerts-deprecation-warning />
+
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index b23f8a8eba4..e9d19f18ab5 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -23,6 +23,7 @@ export default () => {
assigneeUsernameQuery,
alertManagementEnabled,
userCanEnableAlertManagement,
+ hasManagedPrometheus,
} = domEl.dataset;
const apolloProvider = new VueApollo({
@@ -64,6 +65,7 @@ export default () => {
alertManagementEnabled: parseBoolean(alertManagementEnabled),
trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
+ hasManagedPrometheus: parseBoolean(hasManagedPrometheus),
},
apolloProvider,
render(createElement) {
diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
index 5171588eb64..2733a59f62d 100644
--- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
+++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue
@@ -12,7 +12,11 @@ import Vue from 'vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import { mappingFields } from '../constants';
-import { getMappingData, transformForSave } from '../utils/mapping_transformations';
+import {
+ getMappingData,
+ transformForSave,
+ setFieldsLabels,
+} from '../utils/mapping_transformations';
export const i18n = {
columns: {
@@ -72,11 +76,14 @@ export default {
},
computed: {
mappingData() {
- return getMappingData(this.gitlabFields, this.parsedPayload, this.savedMapping);
+ return getMappingData(this.gitlabFields, this.formattedParsedPayload, this.savedMapping);
},
hasFallbackColumn() {
return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
},
+ formattedParsedPayload() {
+ return setFieldsLabels(this.parsedPayload);
+ },
},
methods: {
setMapping(gitlabKey, mappingKey, valueKey = mappingFields.mapping) {
@@ -92,14 +99,16 @@ export default {
},
filterFields(searchTerm = '', fields) {
const search = searchTerm.toLowerCase();
- return fields.filter((field) => field.label.toLowerCase().includes(search));
+ return fields.filter((field) =>
+ field.displayLabel.replace('...', '').toLowerCase().includes(search),
+ );
},
isSelected(fieldValue, mapping) {
return isEqual(fieldValue, mapping);
},
selectedValue(mapping) {
return (
- this.parsedPayload.find((item) => isEqual(item.path, mapping))?.label ||
+ this.formattedParsedPayload.find((item) => isEqual(item.path, mapping))?.displayLabel ||
this.$options.i18n.makeSelection
);
},
@@ -167,11 +176,13 @@ export default {
<gl-dropdown-item
v-for="mappingField in filterFields(gitlabField.searchTerm, gitlabField.mappingFields)"
:key="`${mappingField.path}__mapping`"
+ v-gl-tooltip
:is-checked="isSelected(gitlabField.mapping, mappingField.path)"
is-check-item
+ :title="mappingField.tooltip"
@click="setMapping(gitlabField.name, mappingField.path)"
>
- {{ mappingField.label }}
+ {{ mappingField.displayLabel }}
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults(gitlabField.searchTerm, gitlabField.mappingFields)">
{{ $options.i18n.noResults }}
@@ -197,13 +208,15 @@ export default {
gitlabField.mappingFields,
)"
:key="`${mappingField.path}__fallback`"
+ v-gl-tooltip
:is-checked="isSelected(gitlabField.fallback, mappingField.path)"
is-check-item
+ :title="mappingField.tooltip"
@click="
setMapping(gitlabField.name, mappingField.path, $options.mappingFields.fallback)
"
>
- {{ mappingField.label }}
+ {{ mappingField.displayLabel }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="noResults(gitlabField.fallbackSearchTerm, gitlabField.mappingFields)"
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 ef29fc5e8b4..d9e5878b9e3 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -116,7 +116,7 @@ export default {
methods: {
tbodyTrClass(item) {
return {
- [bodyTrClass]: this.integrations.length,
+ [bodyTrClass]: this.integrations?.length,
'gl-bg-blue-50': (item !== null && item.id) === this.currentIntegration?.id,
};
},
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 f51c8d7e9f7..3917e4c5fdd 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -14,13 +14,12 @@ import updateCurrentHttpIntegrationMutation from '../graphql/mutations/update_cu
import updateCurrentPrometheusIntegrationMutation from '../graphql/mutations/update_current_prometheus_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
-import getHttpIntegrationsQuery from '../graphql/queries/get_http_integrations.query.graphql';
+import getHttpIntegrationQuery from '../graphql/queries/get_http_integration.query.graphql';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import service from '../services';
import {
updateStoreAfterIntegrationDelete,
updateStoreAfterIntegrationAdd,
- updateStoreAfterHttpIntegrationAdd,
} from '../utils/cache_updates';
import {
DELETE_INTEGRATION_ERROR,
@@ -68,33 +67,8 @@ export default {
};
},
update(data) {
- const { alertManagementIntegrations: { nodes: list = [] } = {} } = data.project || {};
-
- return {
- list,
- };
- },
- error(err) {
- createFlash({ message: err });
- },
- },
- // TODO: we'll need to update the logic to request specific http integration by its id on edit
- // when BE adds support for it https://gitlab.com/gitlab-org/gitlab/-/issues/321674
- // currently the request for ALL http integrations is made and on specific integration edit we search it in the list
- httpIntegrations: {
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query: getHttpIntegrationsQuery,
- variables() {
- return {
- projectPath: this.projectPath,
- };
- },
- update(data) {
- const { alertManagementHttpIntegrations: { nodes: list = [] } = {} } = data.project || {};
-
- return {
- list,
- };
+ const { alertManagementIntegrations: { nodes = [] } = {} } = data.project || {};
+ return nodes;
},
error(err) {
createFlash({ message: err });
@@ -107,9 +81,9 @@ export default {
data() {
return {
isUpdating: false,
- integrations: {},
- httpIntegrations: {},
+ integrations: [],
currentIntegration: null,
+ currentHttpIntegration: null,
newIntegration: null,
formVisible: false,
showSuccessfulCreateAlert: false,
@@ -121,7 +95,7 @@ export default {
return this.$apollo.queries.integrations.loading;
},
canAddIntegration() {
- return this.multiIntegrations || this.integrations?.list?.length < 2;
+ return this.multiIntegrations || this.integrations.length < 2;
},
},
methods: {
@@ -142,11 +116,6 @@ export default {
},
update(store, { data }) {
updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
- if (isHttp) {
- updateStoreAfterHttpIntegrationAdd(store, getHttpIntegrationsQuery, data, {
- projectPath,
- });
- }
},
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
@@ -253,15 +222,38 @@ export default {
});
},
editIntegration({ id, type }) {
- let currentIntegration = this.integrations.list.find((integration) => integration.id === id);
- if (this.isHttp(type)) {
- const httpIntegrationMappingData = this.httpIntegrations.list.find(
- (integration) => integration.id === id,
- );
- currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData };
- }
+ const currentIntegration = this.integrations.find((integration) => integration.id === id);
- this.viewIntegration(currentIntegration, tabIndices.viewCredentials);
+ if (this.multiIntegrations && this.isHttp(type)) {
+ this.$apollo.addSmartQuery('currentHttpIntegration', {
+ query: getHttpIntegrationQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ id,
+ };
+ },
+ update(data) {
+ const {
+ project: {
+ alertManagementHttpIntegrations: { nodes = [{}] },
+ },
+ } = data;
+ return nodes[0];
+ },
+ result() {
+ this.viewIntegration(
+ { ...currentIntegration, ...this.currentHttpIntegration },
+ tabIndices.viewCredentials,
+ );
+ },
+ error() {
+ createFlash({ message: DEFAULT_ERROR });
+ },
+ });
+ } else {
+ this.viewIntegration(currentIntegration, tabIndices.viewCredentials);
+ }
},
viewIntegration(integration, tabIndex) {
this.$apollo
@@ -368,7 +360,7 @@ export default {
</gl-alert>
<integrations-list
- :integrations="integrations.list"
+ :integrations="integrations"
:loading="loading"
@edit-integration="editIntegration"
@delete-integration="deleteIntegration"
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql
index 5bd63820629..e9230812db2 100644
--- a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_prometheus_integration.mutation.graphql
@@ -6,7 +6,6 @@ mutation updateCurrentPrometheusIntegration(
$type: String
$url: String
$apiUrl: String
- $samplePayload: String
) {
updateCurrentIntegration(
id: $id
@@ -16,6 +15,5 @@ mutation updateCurrentPrometheusIntegration(
type: $type
url: $url
apiUrl: $apiUrl
- samplePayload: $samplePayload
) @client
}
diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
index 833a2d6c12f..d20a8b8334b 100644
--- a/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integrations.query.graphql
+++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_http_integration.query.graphql
@@ -1,9 +1,8 @@
#import "ee_else_ce/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql"
-# TODO: this query need to accept http integration id to request a sepcific integration
-query getHttpIntegrations($projectPath: ID!) {
+query getHttpIntegration($projectPath: ID!, $id: ID) {
project(fullPath: $projectPath) {
- alertManagementHttpIntegrations {
+ alertManagementHttpIntegrations(id: $id) {
nodes {
...HttpIntegrationPayloadData
}
diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
index 716c709a931..a50b6515afa 100644
--- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js
+++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
@@ -58,31 +58,6 @@ const addIntegrationToStore = (
});
};
-const addHttpIntegrationToStore = (store, query, { httpIntegrationCreate }, variables) => {
- const integration = httpIntegrationCreate?.integration;
- if (!integration) {
- return;
- }
-
- const sourceData = store.readQuery({
- query,
- variables,
- });
-
- const data = produce(sourceData, (draftData) => {
- draftData.project.alertManagementHttpIntegrations.nodes = [
- integration,
- ...draftData.project.alertManagementHttpIntegrations.nodes,
- ];
- });
-
- store.writeQuery({
- query,
- variables,
- data,
- });
-};
-
const onError = (data, message) => {
createFlash({ message });
throw new Error(data.errors);
@@ -105,11 +80,3 @@ export const updateStoreAfterIntegrationAdd = (store, query, data, variables) =>
addIntegrationToStore(store, query, data, variables);
}
};
-
-export const updateStoreAfterHttpIntegrationAdd = (store, query, data, variables) => {
- if (hasErrors(data)) {
- onError(data, ADD_INTEGRATION_ERROR);
- } else {
- addHttpIntegrationToStore(store, query, data, variables);
- }
-};
diff --git a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
index 5c4b9bcd505..ed126dfafd6 100644
--- a/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
+++ b/app/assets/javascripts/alerts_settings/utils/mapping_transformations.js
@@ -1,4 +1,6 @@
import { isEqual } from 'lodash';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
/**
* Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
* creates an object in a form convenient to build UI && interact with it
@@ -32,6 +34,26 @@ export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
});
};
+export const setFieldsLabels = (fields) => {
+ return fields.map((field) => {
+ const { label } = field;
+ let displayLabel;
+ let tooltip;
+ const labels = label.split('/');
+ if (labels.length > 1) {
+ tooltip = labels.join('.');
+ displayLabel = `...${capitalizeFirstCharacter(labels.pop())}`;
+ } else {
+ displayLabel = capitalizeFirstCharacter(label);
+ }
+
+ return {
+ ...field,
+ displayLabel,
+ tooltip,
+ };
+ });
+};
/**
* Based on mapping data configured by the user creates an object in a format suitable for save on BE
* @param {Object} mappingData - structure describing mapping between GitLab fields and parsed payload fields
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
new file mode 100644
index 00000000000..1a3289ffb75
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { sprintf, s__ } from '~/locale';
+
+const defaultHeaderAttrs = {
+ thClass: 'gl-bg-white!',
+ thAttr: { 'data-testid': 'header' },
+};
+
+export default {
+ components: {
+ GlBadge,
+ GlTable,
+ GlSingleStat,
+ GlLink,
+ GlEmptyState,
+ },
+ inject: {
+ devopsScoreMetrics: {
+ default: null,
+ },
+ devopsReportDocsPath: {
+ default: '',
+ },
+ noDataImagePath: {
+ default: '',
+ },
+ },
+ computed: {
+ titleHelperText() {
+ return sprintf(
+ s__(
+ 'DevopsReport|DevOps score metrics are based on usage over the last 30 days. Last updated: %{timestamp}.',
+ ),
+ { timestamp: this.devopsScoreMetrics.createdAt },
+ );
+ },
+ isEmpty() {
+ return this.devopsScoreMetrics.averageScore === undefined;
+ },
+ },
+ tableHeaderFields: [
+ {
+ key: 'title',
+ label: '',
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'usage',
+ label: s__('DevopsReport|Your usage'),
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'leadInstance',
+ label: s__('DevopsReport|Leader usage'),
+ ...defaultHeaderAttrs,
+ },
+ {
+ key: 'score',
+ label: s__('DevopsReport|Score'),
+ ...defaultHeaderAttrs,
+ },
+ ],
+};
+</script>
+<template>
+ <gl-empty-state
+ v-if="isEmpty"
+ :title="__('Data is still calculating...')"
+ :svg-path="noDataImagePath"
+ >
+ <template #description>
+ <p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
+ <gl-link :href="devopsReportDocsPath">{{
+ __('See example DevOps Score page in our documentation.')
+ }}</gl-link>
+ </template>
+ </gl-empty-state>
+ <div v-else data-testid="devops-score-app">
+ <div class="gl-text-gray-400 gl-my-4" data-testid="devops-score-note-text">
+ {{ titleHelperText }}
+ </div>
+ <gl-single-stat
+ unit="%"
+ size="sm"
+ :title="s__('DevopsReport|Your score')"
+ :should-animate="true"
+ :value="devopsScoreMetrics.averageScore.value"
+ :meta-icon="devopsScoreMetrics.averageScore.scoreLevel.icon"
+ :meta-text="devopsScoreMetrics.averageScore.scoreLevel.label"
+ :variant="devopsScoreMetrics.averageScore.scoreLevel.variant"
+ />
+ <gl-table
+ :fields="$options.tableHeaderFields"
+ :items="devopsScoreMetrics.cards"
+ thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
+ stacked="sm"
+ >
+ <template #cell(usage)="{ item }">
+ <div data-testid="usageCol">
+ <span>{{ item.usage }}</span>
+ <gl-badge :variant="item.scoreLevel.variant" size="sm" class="gl-ml-1">{{
+ item.scoreLevel.label
+ }}</gl-badge>
+ </div>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_report/devops_score.js
new file mode 100644
index 00000000000..18f7cf0c3ab
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/devops_score.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DevopsScore from './components/devops_score.vue';
+
+export default () => {
+ const el = document.getElementById('js-devops-score');
+
+ if (!el) return false;
+
+ const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: {
+ devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
+ devopsReportDocsPath,
+ noDataImagePath,
+ },
+ render(h) {
+ return h(DevopsScore);
+ },
+ });
+};
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
index 0cb8d9be0e4..0131407e723 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_empty_state.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
@@ -6,7 +6,7 @@ export default () => {
// eslint-disable-next-line no-new
new UserCallout();
- const emptyStateContainer = document.getElementById('js-devops-empty-state');
+ const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
if (!emptyStateContainer) return false;
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index 55642aa64db..f89600fbed3 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
+import { GlSprintf, GlButton, GlAlert, GlCard } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -34,7 +34,7 @@ export default {
recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME,
i18n,
mousetrap: null,
- components: { GlSprintf, GlButton, GlAlert, ClipboardButton },
+ components: { GlSprintf, GlButton, GlAlert, ClipboardButton, GlCard },
mixins: [Tracking.mixin()],
props: {
codes: {
@@ -116,8 +116,8 @@ export default {
</gl-sprintf>
</p>
- <div
- class="codes-to-print gl-my-5 gl-p-5 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base"
+ <gl-card
+ class="codes-to-print gl-my-5"
data-testid="recovery-codes"
data-qa-selector="codes_content"
>
@@ -126,7 +126,7 @@ export default {
<span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span>
</li>
</ul>
- </div>
+ </gl-card>
<div class="gl-my-n2 gl-mx-n2 gl-display-flex gl-flex-wrap">
<div class="gl-p-2">
<clipboard-button
@@ -140,6 +140,7 @@ export default {
</div>
<div class="gl-p-2">
<gl-button
+ is-unsafe-link
:href="codeDownloadUrl"
:title="$options.i18n.downloadButton"
icon="download"
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 3a2f2078e44..43f44370af8 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -12,7 +12,6 @@ import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
-window.axios = axios;
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index f16a547e441..86c7b4c7a6e 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { mapState } from 'vuex';
import { GROUP_BADGE } from '../constants';
import BadgeListRow from './badge_list_row.vue';
@@ -9,6 +9,7 @@ export default {
components: {
BadgeListRow,
GlLoadingIcon,
+ GlBadge,
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
@@ -26,7 +27,7 @@ export default {
<div class="card">
<div class="card-header">
{{ s__('Badges|Your badges') }}
- <span v-show="!isLoading" class="badge badge-pill">{{ badges.length }}</span>
+ <gl-badge v-show="!isLoading" size="sm">{{ badges.length }}</gl-badge>
</div>
<gl-loading-icon v-show="isLoading" size="lg" class="card-body" />
<div v-if="hasNoBadges" class="card-body">
diff --git a/app/assets/javascripts/batch_comments/components/drafts_count.vue b/app/assets/javascripts/batch_comments/components/drafts_count.vue
index 5e110b101eb..61718b766d8 100644
--- a/app/assets/javascripts/batch_comments/components/drafts_count.vue
+++ b/app/assets/javascripts/batch_comments/components/drafts_count.vue
@@ -12,7 +12,7 @@ export default {
};
</script>
<template>
- <gl-badge size="sm" variant="success">
+ <gl-badge size="sm" variant="info" class="gl-ml-2">
{{ draftsCount }}
<span class="sr-only"> {{ n__('draft', 'drafts', draftsCount) }} </span>
</gl-badge>
diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
index fb643d441ec..91b3b6a685c 100644
--- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mapActions, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import PreviewItem from './preview_item.vue';
export default {
@@ -11,13 +11,22 @@ export default {
PreviewItem,
},
computed: {
+ ...mapState('diffs', ['viewDiffsFileByFile']),
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
},
methods: {
+ ...mapActions('diffs', ['toggleActiveFileByHash']),
...mapActions('batchComments', ['scrollToDraft']),
isLast(index) {
return index === this.sortedDrafts.length - 1;
},
+ async onClickDraft(draft) {
+ if (this.viewDiffsFileByFile && draft.file_hash) {
+ await this.toggleActiveFileByHash(draft.file_hash);
+ }
+
+ await this.scrollToDraft(draft);
+ },
},
};
</script>
@@ -26,7 +35,7 @@ export default {
<gl-dropdown
:header-text="n__('%d pending comment', '%d pending comments', draftsCount)"
dropup
- toggle-class="qa-review-preview-toggle"
+ data-qa-selector="review_preview_dropdown"
>
<template #button-content>
{{ __('Pending comments') }}
@@ -35,7 +44,8 @@ export default {
<gl-dropdown-item
v-for="(draft, index) in sortedDrafts"
:key="draft.id"
- @click="scrollToDraft(draft)"
+ data-testid="preview-item"
+ @click="onClickDraft(draft)"
>
<preview-item :draft="draft" :is-last="isLast(index)" />
</gl-dropdown-item>
diff --git a/app/assets/javascripts/batch_comments/components/publish_button.vue b/app/assets/javascripts/batch_comments/components/publish_button.vue
index 2a7be605003..d4fc4ad744a 100644
--- a/app/assets/javascripts/batch_comments/components/publish_button.vue
+++ b/app/assets/javascripts/batch_comments/components/publish_button.vue
@@ -22,7 +22,7 @@ export default {
variant: {
type: String,
required: false,
- default: 'success',
+ default: 'confirm',
},
},
computed: {
diff --git a/app/assets/javascripts/behaviors/date_picker.js b/app/assets/javascripts/behaviors/date_picker.js
new file mode 100644
index 00000000000..efd89ec4330
--- /dev/null
+++ b/app/assets/javascripts/behaviors/date_picker.js
@@ -0,0 +1,33 @@
+import $ from 'jquery';
+import Pikaday from 'pikaday';
+import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
+
+export default function initDatePickers() {
+ $('.datepicker').each(function initPikaday() {
+ const $datePicker = $(this);
+ const datePickerVal = $datePicker.val();
+
+ const calendar = new Pikaday({
+ field: $datePicker.get(0),
+ theme: 'gitlab-theme animate-picker',
+ format: 'yyyy-mm-dd',
+ container: $datePicker.parent().get(0),
+ parse: (dateString) => parsePikadayDate(dateString),
+ toString: (date) => pikadayToString(date),
+ onSelect(dateText) {
+ $datePicker.val(calendar.toString(dateText));
+ },
+ firstDay: gon.first_day_of_week,
+ });
+
+ calendar.setDate(parsePikadayDate(datePickerVal));
+
+ $datePicker.data('pikaday', calendar);
+ });
+
+ $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ e.preventDefault();
+ const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ calendar.setDate(null);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 8238f5523f3..12f47255bdf 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -114,6 +114,12 @@ class SafeMathRenderer {
throwOnError: true,
maxSize: 20,
maxExpand: 20,
+ trust: (context) =>
+ // this config option restores the KaTeX pre-v0.11.0
+ // behavior of allowing certain commands and protocols
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ ['\\url', '\\href'].includes(context.command) &&
+ ['http', 'https', 'mailto', '_relative'].includes(context.protocol),
});
} catch (e) {
// Don't show a flash for now because it would override an existing flash message
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 5b5148a850b..f5b2d266c18 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { once } from 'lodash';
+import { once, countBy } from 'lodash';
import { deprecatedCreateFlash as flash } from '~/flash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { __, sprintf } from '~/locale';
@@ -22,6 +22,8 @@ import { __, sprintf } from '~/locale';
const MAX_CHAR_LIMIT = 2000;
// Max # of mermaid blocks that can be rendered in a page.
const MAX_MERMAID_BLOCK_LIMIT = 50;
+// Max # of `&` allowed in Chaining of links syntax
+const MAX_CHAINING_OF_LINKS_LIMIT = 30;
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
@@ -64,6 +66,18 @@ function importMermaidModule() {
});
}
+function shouldLazyLoadMermaidBlock(source) {
+ /**
+ * If source contains `&`, which means that it might
+ * contain Chaining of links a new syntax in Mermaid.
+ */
+ if (countBy(source)['&'] > MAX_CHAINING_OF_LINKS_LIMIT) {
+ return true;
+ }
+
+ return false;
+}
+
function fixElementSource(el) {
// Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
const source = el.textContent.replace(/<br\s*\/>/g, '<br>');
@@ -128,7 +142,8 @@ function renderMermaids($els) {
if (
(source && source.length > MAX_CHAR_LIMIT) ||
renderedChars > MAX_CHAR_LIMIT ||
- renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT
+ renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
+ shouldLazyLoadMermaidBlock(source)
) {
const html = `
<div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 6abbd7f3243..c63dba05f10 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -375,7 +375,7 @@ export const MR_PREVIOUS_FILE_IN_DIFF = {
export const MR_GO_TO_FILE = {
id: 'mergeRequests.goToFile',
description: __('Go to file'),
- defaultKeys: ['t', 'mod+p'],
+ defaultKeys: ['mod+p', 't'],
customizable: false,
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcut.vue b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue
new file mode 100644
index 00000000000..e5992779a99
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue
@@ -0,0 +1,80 @@
+<script>
+import { __, s__ } from '~/locale';
+
+// Map some keys to their proper representation depending on the system
+// See also: https://craig.is/killing/mice#keys
+const getKeyMap = () => {
+ const keyMap = {
+ up: '↑',
+ down: '↓',
+ left: '←',
+ right: '→',
+ ctrl: s__('KeyboardKey|Ctrl'),
+ shift: s__('KeyboardKey|Shift'),
+ enter: s__('KeyboardKey|Enter'),
+ esc: s__('KeyboardKey|Esc'),
+ command: '⌘',
+ option: window.gl?.client?.isMac ? '⌥' : s__('KeyboardKey|Alt'),
+ };
+
+ // Meta and alt are aliases
+ keyMap.meta = keyMap.command;
+ keyMap.alt = keyMap.option;
+
+ // Mod is Command on Mac, and Ctrl on Windows/Linux
+ keyMap.mod = window.gl?.client?.isMac ? keyMap.command : keyMap.ctrl;
+
+ return keyMap;
+};
+
+export default {
+ functional: true,
+ props: {
+ shortcuts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ render(createElement, context) {
+ const keyMap = getKeyMap();
+
+ const { staticClass } = context.data;
+
+ const shortcuts = context.props.shortcuts.reduce((acc, shortcut, i) => {
+ if (
+ !window.gl?.client?.isMac &&
+ (shortcut.includes('command') || shortcut.includes('meta'))
+ ) {
+ return acc;
+ }
+ const keys = shortcut.split(/([ +])/);
+
+ if (i !== 0 && acc.length) {
+ acc.push(` ${__('or')} `);
+ // If there are multiple alternative shortcuts,
+ // we keep them on the same line if they are single-key, e.g. `]` or `j`
+ // but if they consist of multiple keys, we insert a line break, e.g.:
+ // `shift` + `]` <br> or `shift` + `j`
+ if (keys.length > 1) {
+ acc.push(createElement('br'));
+ }
+ }
+
+ keys.forEach((key) => {
+ if (key === '+') {
+ acc.push(' + ');
+ } else if (key === ' ') {
+ acc.push(` ${__('then')} `);
+ } else {
+ acc.push(createElement('kbd', {}, [keyMap[key] ?? key]));
+ }
+ });
+
+ return acc;
+ }, []);
+
+ return createElement('div', { staticClass }, shortcuts);
+ },
+};
+</script>
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
index 49216cc4aa0..cb7c6f9f6bc 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -1,525 +1,99 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlIcon, GlModal } from '@gitlab/ui';
+import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { keybindingGroups } from './keybindings';
+import Shortcut from './shortcut.vue';
import ShortcutsToggle from './shortcuts_toggle.vue';
export default {
components: {
- GlIcon,
GlModal,
+ GlSearchBoxByType,
ShortcutsToggle,
+ Shortcut,
+ },
+ data() {
+ return {
+ searchTerm: '',
+ };
},
computed: {
- ctrlCharacter() {
- return window.gl.client.isMac ? '⌘' : 'ctrl';
- },
- onDotCom() {
- return window.gon.dot_com;
+ filteredKeybindings() {
+ if (!this.searchTerm) {
+ return keybindingGroups;
+ }
+
+ const search = this.searchTerm.toLocaleLowerCase();
+
+ const mapped = keybindingGroups.map((group) => {
+ if (group.name.toLocaleLowerCase().includes(search)) {
+ return group;
+ }
+ return {
+ ...group,
+ keybindings: group.keybindings.filter((binding) =>
+ binding.description.toLocaleLowerCase().includes(search),
+ ),
+ };
+ });
+
+ return mapped.filter((group) => group.keybindings.length);
},
},
+ i18n: {
+ title: __(`Keyboard shortcuts`),
+ search: s__(`KeyboardShortcuts|Search keyboard shortcuts`),
+ noMatch: s__(`KeyboardShortcuts|No shortcuts matched your search`),
+ },
};
</script>
<template>
<gl-modal
modal-id="keyboard-shortcut-modal"
size="lg"
+ :title="$options.i18n.title"
data-testid="modal-shortcuts"
+ body-class="shortcut-help-body gl-p-0!"
:visible="true"
:hide-footer="true"
@hidden="$emit('hidden')"
>
- <template #modal-title>
- <shortcuts-toggle />
- </template>
- <div class="row">
- <div class="col-lg-4">
- <table class="shortcut-mappings text-2">
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Global Shortcuts') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>?</kbd>
- </td>
- <td>{{ __('Toggle this dialog') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift p</kbd>
- </td>
- <td>{{ __('Go to your projects') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift g</kbd>
- </td>
- <td>{{ __('Go to your groups') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift a</kbd>
- </td>
- <td>{{ __('Go to the activity feed') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift l</kbd>
- </td>
- <td>{{ __('Go to the milestone list') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift s</kbd>
- </td>
- <td>{{ __('Go to your snippets') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>s</kbd>
- /
- <kbd>/</kbd>
- </td>
- <td>{{ __('Start search') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift i</kbd>
- </td>
- <td>{{ __('Go to your issues') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift m</kbd>
- </td>
- <td>{{ __('Go to your merge requests') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>shift t</kbd>
- </td>
- <td>{{ __('Go to your To-Do list') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>p</kbd>
- <kbd>b</kbd>
- </td>
- <td>{{ __('Toggle the Performance Bar') }}</td>
- </tr>
- <tr v-if="onDotCom">
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>x</kbd>
- </td>
- <td>{{ __('Toggle GitLab Next') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Editing') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>{{ ctrlCharacter }} shift p</kbd>
- </td>
- <td>{{ __('Toggle Markdown preview') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-up" />
- </kbd>
- </td>
- <td>
- {{ __('Edit your most recent comment in a thread (from an empty textarea)') }}
- </td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Wiki') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>e</kbd>
- </td>
- <td>{{ __('Edit wiki page') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Repository Graph') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-left" />
- </kbd>
- /
- <kbd>h</kbd>
- </td>
- <td>{{ __('Scroll left') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-right" />
- </kbd>
- /
- <kbd>l</kbd>
- </td>
- <td>{{ __('Scroll right') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-up" />
- </kbd>
- /
- <kbd>k</kbd>
- </td>
- <td>{{ __('Scroll up') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-down" />
- </kbd>
- /
- <kbd>j</kbd>
- </td>
- <td>{{ __('Scroll down') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- shift
- <gl-icon name="arrow-up" />
- / k
- </kbd>
- </td>
- <td>{{ __('Scroll to top') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- shift
- <gl-icon name="arrow-down" />
- / j
- </kbd>
- </td>
- <td>{{ __('Scroll to bottom') }}</td>
- </tr>
- </tbody>
- </table>
- </div>
- <div class="col-lg-4">
- <table class="shortcut-mappings text-2">
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Project') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>p</kbd>
- </td>
- <td>{{ __("Go to the project's overview page") }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>v</kbd>
- </td>
- <td>{{ __("Go to the project's activity feed") }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>r</kbd>
- </td>
- <td>{{ __('Go to releases') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>f</kbd>
- </td>
- <td>{{ __('Go to files') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>t</kbd>
- </td>
- <td>{{ __('Go to find file') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>c</kbd>
- </td>
- <td>{{ __('Go to commits') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>n</kbd>
- </td>
- <td>{{ __('Go to repository graph') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>d</kbd>
- </td>
- <td>{{ __('Go to repository charts') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>i</kbd>
- </td>
- <td>{{ __('Go to issues') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>i</kbd>
- </td>
- <td>{{ __('New issue') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>b</kbd>
- </td>
- <td>{{ __('Go to issue boards') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>m</kbd>
- </td>
- <td>{{ __('Go to merge requests') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>j</kbd>
- </td>
- <td>{{ __('Go to jobs') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>l</kbd>
- </td>
- <td>{{ __('Go to metrics') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>e</kbd>
- </td>
- <td>{{ __('Go to environments') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>k</kbd>
- </td>
- <td>{{ __('Go to kubernetes') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>s</kbd>
- </td>
- <td>{{ __('Go to snippets') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>g</kbd>
- <kbd>w</kbd>
- </td>
- <td>{{ __('Go to wiki') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Project Files') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-up" />
- </kbd>
- </td>
- <td>{{ __('Move selection up') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>
- <gl-icon name="arrow-down" />
- </kbd>
- </td>
- <td>{{ __('Move selection down') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>enter</kbd>
- </td>
- <td>{{ __('Open Selection') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>esc</kbd>
- </td>
- <td>{{ __('Go back (while searching for files)') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>y</kbd>
- </td>
- <td>{{ __('Go to file permalink (while viewing a file)') }}</td>
- </tr>
- </tbody>
- </table>
- </div>
- <div class="col-lg-4">
- <table class="shortcut-mappings text-2">
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Epics, issues, and merge requests') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>r</kbd>
- </td>
- <td>{{ __('Comment/Reply (quoting selected text)') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>e</kbd>
- </td>
- <td>{{ __('Edit description') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>l</kbd>
- </td>
- <td>{{ __('Change label') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Issues and merge requests') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>a</kbd>
- </td>
- <td>{{ __('Change assignee') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>m</kbd>
- </td>
- <td>{{ __('Change milestone') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Merge requests') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>]</kbd>
- /
- <kbd>j</kbd>
- </td>
- <td>{{ __('Next file in diff') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>[</kbd>
- /
- <kbd>k</kbd>
- </td>
- <td>{{ __('Previous file in diff') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>{{ ctrlCharacter }} p</kbd>
- </td>
- <td>{{ __('Go to file') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>n</kbd>
- </td>
- <td>{{ __('Next unresolved discussion') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>p</kbd>
- </td>
- <td>{{ __('Previous unresolved discussion') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>b</kbd>
- </td>
- <td>{{ __('Copy source branch name') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Merge request commits') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>c</kbd>
- </td>
- <td>{{ __('Next commit') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>x</kbd>
- </td>
- <td>{{ __('Previous commit') }}</td>
- </tr>
- </tbody>
- <tbody>
- <tr>
- <th></th>
- <th>{{ __('Web IDE') }}</th>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>{{ ctrlCharacter }} p</kbd>
- </td>
- <td>{{ __('Go to file') }}</td>
- </tr>
- <tr>
- <td class="shortcut">
- <kbd>{{ ctrlCharacter }} enter</kbd>
- </td>
- <td>{{ __('Commit (when editing commit message)') }}</td>
- </tr>
- </tbody>
- </table>
- </div>
+ <div
+ class="gl-sticky gl-top-0 gl-py-5 gl-px-5 gl-display-flex gl-align-items-center gl-bg-white"
+ >
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :aria-label="$options.i18n.search"
+ class="gl-w-half gl-mr-3"
+ />
+ <shortcuts-toggle class="gl-w-half gl-ml-3" />
+ </div>
+ <div v-if="filteredKeybindings.length === 0" class="gl-px-5">
+ {{ $options.i18n.noMatch }}
+ </div>
+ <div v-else class="shortcut-help-container gl-mt-8 gl-px-5 gl-pb-5">
+ <section
+ v-for="group in filteredKeybindings"
+ :key="group.id"
+ class="shortcut-help-mapping gl-mb-4"
+ >
+ <strong class="shortcut-help-mapping-title gl-w-half gl-display-inline-block">
+ {{ group.name }}
+ </strong>
+ <div
+ v-for="keybinding in group.keybindings"
+ :key="keybinding.id"
+ class="gl-display-flex gl-align-items-center"
+ >
+ <shortcut
+ class="gl-w-40p gl-flex-shrink-0 gl-text-right gl-pr-4"
+ :shortcuts="keybinding.defaultKeys"
+ />
+ <div class="gl-w-half gl-flex-shrink-0 gl-flex-grow-1">
+ {{ keybinding.description }}
+ </div>
+ </div>
+ </section>
</div>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
index 6cbe443062a..8f1518a1c9c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
@@ -6,7 +6,7 @@ import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './sho
export default {
i18n: {
- toggleLabel: __('Keyboard shortcuts'),
+ toggleLabel: __('Toggle shortcuts'),
},
components: {
GlToggle,
@@ -31,14 +31,12 @@ export default {
</script>
<template>
- <div v-if="localStorageUsable" class="d-inline-flex align-items-center js-toggle-shortcuts">
+ <div v-if="localStorageUsable" class="js-toggle-shortcuts">
<gl-toggle
v-model="shortcutsEnabled"
- aria-describedby="shortcutsToggle"
:label="$options.i18n.toggleLabel"
label-position="left"
@change="onChange"
/>
- <div id="shortcutsToggle" class="sr-only">{{ __('Enable or disable keyboard shortcuts') }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index f5f06436bcc..60729c11002 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -6,6 +6,7 @@ import BlobContentError from './blob_content_error.vue';
import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from './constants';
export default {
+ name: 'BlobContent',
components: {
GlLoadingIcon,
BlobContentError,
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 77910850908..59ab84bf208 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -7,7 +7,6 @@ import toast from '~/vue_shared/plugins/global_toast';
import { deprecatedCreateFlash as Flash } from '../flash';
-import BlobCiSyntaxYamlSelector from './template_selectors/ci_syntax_yaml_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
@@ -34,7 +33,6 @@ export default class FileTemplateMediator {
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
- BlobCiSyntaxYamlSelector,
MetricsDashboardSelector,
DockerfileSelector,
LicenseSelector,
diff --git a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
deleted file mode 100644
index c30ff4f1290..00000000000
--- a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import FileTemplateSelector from '../file_template_selector';
-
-export default class BlobCiSyntaxYamlSelector extends FileTemplateSelector {
- constructor({ mediator }) {
- super(mediator);
- this.config = {
- key: 'gitlab-ci-yaml',
- name: '.gitlab-ci.yml',
- pattern: /(.gitlab-ci.yml)/,
- type: 'gitlab_ci_syntax_ymls',
- dropdown: '.js-gitlab-ci-syntax-yml-selector',
- wrapper: '.js-gitlab-ci-syntax-yml-selector-wrap',
- };
- }
-
- initDropdown() {
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.$dropdown.data('data'),
- filterable: true,
- selectable: true,
- search: {
- fields: ['name'],
- },
- clicked: (options) => this.reportSelectionName(options),
- text: (item) => item.name,
- });
- }
-}
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 4741152afce..22c6b31143f 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { __ } from '~/locale';
+import {
+ REPO_BLOB_LOAD_VIEWER_START,
+ REPO_BLOB_LOAD_VIEWER_FINISH,
+ REPO_BLOB_LOAD_VIEWER,
+} from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import { fixTitle } from '~/tooltips';
import { deprecatedCreateFlash as Flash } from '../../flash';
import axios from '../../lib/utils/axios_utils';
@@ -130,6 +136,9 @@ export default class BlobViewer {
}
switchToViewer(name) {
+ performanceMarkAndMeasure({
+ mark: REPO_BLOB_LOAD_VIEWER_START,
+ });
const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
@@ -163,6 +172,15 @@ export default class BlobViewer {
handleLocationHash();
this.toggleCopyButtonState();
+ performanceMarkAndMeasure({
+ mark: REPO_BLOB_LOAD_VIEWER_FINISH,
+ measures: [
+ {
+ name: REPO_BLOB_LOAD_VIEWER,
+ start: REPO_BLOB_LOAD_VIEWER_START,
+ },
+ ],
+ });
})
.catch(() => new Flash(__('Error loading viewer')));
}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 173c82ef9b0..d26af07d54f 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
+import initCodeQualityWalkthrough from '~/code_quality_walkthrough';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
@@ -38,6 +39,13 @@ const initPopovers = () => {
}
};
+const initCodeQualityWalkthroughStep = () => {
+ const codeQualityWalkthroughEl = document.querySelector('.js-code-quality-walkthrough');
+ if (codeQualityWalkthroughEl) {
+ initCodeQualityWalkthrough(codeQualityWalkthroughEl);
+ }
+};
+
export const initUploadForm = () => {
const uploadBlobForm = $('.js-upload-blob-form');
if (uploadBlobForm.length) {
@@ -74,6 +82,7 @@ export default () => {
isMarkdown,
});
initPopovers();
+ initCodeQualityWalkthroughStep();
})
.catch((e) => createFlash(e));
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index a8b870f9b8e..f53d41dd0f4 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy, cloneDeep } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ListType, NOT_FILTER } from './constants';
+import { ListType, NOT_FILTER, AssigneeIdParamValues } from './constants';
export function getMilestone() {
return null;
@@ -186,6 +186,35 @@ export function transformNotFilters(filters) {
}, {});
}
+export function getSupportedParams(filters, supportedFilters) {
+ return supportedFilters.reduce((acc, f) => {
+ /**
+ * TODO the API endpoint for the classic boards
+ * accepts assignee wildcard value as 'assigneeId' param -
+ * while the GraphQL query accepts the value in 'assigneWildcardId' field.
+ * Once we deprecate the classics boards,
+ * we should change the filtered search bar to use 'asssigneeWildcardId' as a token name.
+ */
+ if (f === 'assigneeId' && filters[f]) {
+ return AssigneeIdParamValues.includes(filters[f])
+ ? {
+ ...acc,
+ assigneeWildcardId: filters[f].toUpperCase(),
+ }
+ : acc;
+ }
+
+ if (filters[f]) {
+ return {
+ ...acc,
+ [f]: filters[f],
+ };
+ }
+
+ return acc;
+ }, {});
+}
+
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
return '';
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index aacea0b970c..2821b799cef 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -31,7 +31,6 @@ export default {
},
computed: {
...mapState(['selectedBoardItems', 'activeId']),
- ...mapGetters(['isSwimlanesOn']),
isActive() {
return this.item.id === this.activeId;
},
@@ -46,7 +45,7 @@ export default {
...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
toggleIssue(e) {
// Don't do anything if this happened on a no trigger element
- if (e.target.classList.contains('js-no-trigger')) return;
+ if (e.target.closest('.js-no-trigger')) return;
const isMultiSelect = e.ctrlKey || e.metaKey;
if (isMultiSelect) {
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 9ff2cdd76d0..0cb2e64042e 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -190,6 +190,7 @@ export default {
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
+ class="js-no-trigger"
:background-color="label.color"
:title="label.title"
:description="label.description"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index a4b1e6adacf..b8a38d833ad 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -4,7 +4,6 @@ import { sortBy } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
-import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardColumn from './board_column.vue';
@@ -48,7 +47,7 @@ export default {
: this.lists;
},
canDragColumns() {
- return !this.isEpicBoard && this.glFeatures.graphqlBoardLists && this.canAdminList;
+ return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
@@ -73,14 +72,7 @@ export default {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
- handleDragOnStart() {
- sortableStart();
- },
-
handleDragOnEnd(params) {
- sortableEnd();
- if (this.isEpicBoard) return;
-
const { item, newIndex, oldIndex, to } = params;
const listId = item.dataset.id;
@@ -108,7 +100,6 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
- @start="handleDragOnStart"
@end="handleDragOnEnd"
>
<board-column
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 46359cc2bca..e1f8457c0e2 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
-import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default {
headerHeight: `${contentTop()}px`,
@@ -18,10 +18,11 @@ export default {
GlDrawer,
BoardSidebarTitle,
SidebarAssigneesWidget,
+ SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
BoardSidebarDueDate,
- BoardSidebarSubscription,
+ SidebarSubscriptionsWidget,
BoardSidebarMilestoneSelect,
BoardSidebarEpicSelect: () =>
import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
@@ -30,7 +31,20 @@ export default {
SidebarIterationWidget: () =>
import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
},
- mixins: [glFeatureFlagsMixin()],
+ inject: {
+ multipleAssigneesFeatureAvailable: {
+ default: false,
+ },
+ epicFeatureAvailable: {
+ default: false,
+ },
+ iterationFeatureAvailable: {
+ default: false,
+ },
+ weightFeatureAvailable: {
+ default: false,
+ },
+ },
computed: {
...mapGetters([
'isSidebarOpen',
@@ -50,7 +64,7 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItem', 'setAssignees']),
+ ...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
@@ -72,13 +86,14 @@ export default {
:iid="activeBoardItem.iid"
:full-path="fullPath"
:initial-assignees="activeBoardItem.assignees"
- class="assignee"
+ :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
@assignees-updated="setAssignees"
/>
- <board-sidebar-epic-select class="epic" />
+ <board-sidebar-epic-select v-if="epicFeatureAvailable" class="epic" />
<div>
<board-sidebar-milestone-select />
<sidebar-iteration-widget
+ v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
:iterations-workspace-path="groupPathForActiveIssue"
@@ -89,8 +104,19 @@ export default {
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
<board-sidebar-due-date />
<board-sidebar-labels-select class="labels" />
- <board-sidebar-weight-input v-if="glFeatures.issueWeights" class="weight" />
- <board-sidebar-subscription class="subscriptions" />
+ <board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
+ <sidebar-confidentiality-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @confidentialityUpdated="setActiveItemConfidential($event)"
+ />
+ <sidebar-subscriptions-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-notifications"
+ />
</template>
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
new file mode 100644
index 00000000000..e564af0c353
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -0,0 +1,154 @@
+<script>
+import { pickBy } from 'lodash';
+import { mapActions } from 'vuex';
+import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+export default {
+ i18n: {
+ search: __('Search'),
+ label: __('Label'),
+ author: __('Author'),
+ },
+ components: { FilteredSearch },
+ inject: ['initialFilterParams'],
+ props: {
+ tokens: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ filterParams: this.initialFilterParams,
+ };
+ },
+ computed: {
+ urlParams() {
+ const { authorUsername, labelName, search } = this.filterParams;
+ let notParams = {};
+
+ if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
+ notParams = pickBy(
+ {
+ 'not[label_name][]': this.filterParams.not.labelName,
+ 'not[author_username]': this.filterParams.not.authorUsername,
+ },
+ undefined,
+ );
+ }
+
+ return {
+ ...notParams,
+ author_username: authorUsername,
+ 'label_name[]': labelName,
+ search,
+ };
+ },
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ handleFilter(filters) {
+ this.filterParams = this.getFilterParams(filters);
+
+ updateHistory({
+ url: setUrlParams(this.urlParams, window.location.href, true, false, true),
+ title: document.title,
+ replace: true,
+ });
+
+ this.performSearch();
+ },
+ getFilteredSearchValue() {
+ const { authorUsername, labelName, search } = this.filterParams;
+ const filteredSearchValue = [];
+
+ if (authorUsername) {
+ filteredSearchValue.push({
+ type: 'author_username',
+ value: { data: authorUsername, operator: '=' },
+ });
+ }
+
+ if (labelName?.length) {
+ filteredSearchValue.push(
+ ...labelName.map((label) => ({
+ type: 'label_name',
+ value: { data: label, operator: '=' },
+ })),
+ );
+ }
+
+ if (this.filterParams['not[authorUsername]']) {
+ filteredSearchValue.push({
+ type: 'author_username',
+ value: { data: this.filterParams['not[authorUsername]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[labelName]']) {
+ filteredSearchValue.push(
+ ...this.filterParams['not[labelName]'].map((label) => ({
+ type: 'label_name',
+ value: { data: label, operator: '!=' },
+ })),
+ );
+ }
+
+ if (search) {
+ filteredSearchValue.push(search);
+ }
+
+ return filteredSearchValue;
+ },
+ getFilterParams(filters = []) {
+ const notFilters = filters.filter((item) => item.value.operator === '!=');
+ const equalsFilters = filters.filter((item) => item.value.operator === '=');
+
+ return { ...this.generateParams(equalsFilters), not: { ...this.generateParams(notFilters) } };
+ },
+ generateParams(filters = []) {
+ const filterParams = {};
+ const labels = [];
+ const plainText = [];
+
+ filters.forEach((filter) => {
+ switch (filter.type) {
+ case 'author_username':
+ filterParams.authorUsername = filter.value.data;
+ break;
+ case 'label_name':
+ labels.push(filter.value.data);
+ break;
+ case 'filtered-search-term':
+ if (filter.value.data) plainText.push(filter.value.data);
+ break;
+ default:
+ break;
+ }
+ });
+
+ if (labels.length) {
+ filterParams.labelName = labels;
+ }
+
+ if (plainText.length) {
+ filterParams.search = plainText.join(' ');
+ }
+ return filterParams;
+ },
+ },
+};
+</script>
+
+<template>
+ <filtered-search
+ class="gl-w-full"
+ namespace=""
+ :tokens="tokens"
+ :search-input-placeholder="$options.i18n.search"
+ :initial-filter-value="getFilteredSearchValue()"
+ @onFilter="handleFilter"
+ />
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index ca66ad6934a..f94697172ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -161,7 +161,7 @@ export default {
const collapsed = !this.list.collapsed;
this.toggleListCollapsed({ listId: this.list.id, collapsed });
- if (!this.isLoggedIn || this.isEpicBoard) {
+ if (!this.isLoggedIn) {
this.addToLocalStorage();
} else {
this.updateListFunction();
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 997655c346a..3d7f1f38a34 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -29,17 +29,17 @@ export default {
};
},
computed: {
- ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
+ ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
- return this.glFeatures.wipLimits;
+ return this.glFeatures.wipLimits && !this.isEpicBoard;
},
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.boardLists[this.activeId];
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
@@ -71,7 +71,7 @@ export default {
deleteBoard() {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
- if (this.shouldUseGraphQL) {
+ if (this.shouldUseGraphQL || this.isEpicBoard) {
this.removeList(this.activeId);
} else {
this.activeList.destroy();
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 f78be83cd82..919ef0d3783 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
@@ -1,10 +1,12 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
+import Api from '~/api';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
@@ -14,7 +16,13 @@ export default {
LabelsSelect,
GlLabel,
},
- inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
+ inject: {
+ labelsFetchPath: {
+ default: null,
+ },
+ labelsManagePath: {},
+ labelsFilterBasePath: {},
+ },
data() {
return {
loading: false,
@@ -38,6 +46,32 @@ export default {
scoped: isScopedLabel(label),
}));
},
+ fetchPath() {
+ /*
+ Labels fetched in epic boards are always group-level labels
+ and the correct path are passed from the backend (injected through labelsFetchPath)
+
+ For issue boards, we should always include project-level labels and use a different endpoint.
+ (it requires knowing the project path of a selected issue.)
+
+ Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
+ And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
+
+ Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates.
+ 'labels-select' has its own vuex store and initializes the passed props as states
+ and these states aren't reactively bound to the passed props.
+ */
+
+ const projectLabelsFetchPath = mergeUrlParams(
+ { include_ancestor_groups: true },
+ Api.buildUrl(Api.projectLabelsPath).replace(
+ ':namespace_path/:project_path',
+ this.projectPathForActiveIssue,
+ ),
+ );
+
+ return this.labelsFetchPath || projectLabelsFetchPath;
+ },
},
methods: {
...mapActions(['setActiveBoardItemLabels']),
@@ -77,7 +111,12 @@ export default {
</script>
<template>
- <board-editable-item ref="sidebarItem" :title="__('Labels')" :loading="loading">
+ <board-editable-item
+ ref="sidebarItem"
+ :title="__('Labels')"
+ :loading="loading"
+ data-testid="sidebar-labels"
+ >
<template #collapsed>
<gl-label
v-for="label in issueLabels"
@@ -95,12 +134,13 @@ export default {
<template #default="{ edit }">
<labels-select
ref="labelsSelect"
+ :key="fetchPath"
:allow-label-edit="false"
:allow-label-create="false"
:allow-multiselect="true"
:allow-scoped-labels="true"
:selected-labels="selectedLabels"
- :labels-fetch-path="labelsFetchPath"
+ :labels-fetch-path="fetchPath"
:labels-manage-path="labelsManagePath"
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 4ebd30fe67b..d88774d11c1 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,10 +1,28 @@
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
+import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql';
+import updateBoardListMutation from './graphql/board_list_update.mutation.graphql';
+
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
+export const SupportedFilters = [
+ 'assigneeUsername',
+ 'authorUsername',
+ 'labelName',
+ 'milestoneTitle',
+ 'releaseTag',
+ 'search',
+ 'myReactionEmoji',
+ 'assigneeId',
+];
+
+/* eslint-disable-next-line @gitlab/require-i18n-strings */
+export const AssigneeIdParamValues = ['Any', 'None'];
+
export const issuableTypes = {
issue: 'issue',
epic: 'epic',
@@ -46,9 +64,10 @@ export const NOT_FILTER = 'not[';
export const flashAnimationDuration = 2000;
-export default {
- BoardType,
- ListType,
+export const listsQuery = {
+ [issuableTypes.issue]: {
+ query: boardListsQuery,
+ },
};
export const blockingIssuablesQueries = {
@@ -57,6 +76,18 @@ export const blockingIssuablesQueries = {
},
};
+export const updateListQueries = {
+ [issuableTypes.issue]: {
+ mutation: updateBoardListMutation,
+ },
+};
+
+export const deleteListQueries = {
+ [issuableTypes.issue]: {
+ mutation: destroyBoardListMutation,
+ },
+};
+
export const titleQueries = {
[issuableTypes.issue]: {
mutation: issueSetTitleMutation,
@@ -74,3 +105,8 @@ export const subscriptionQueries = {
mutation: updateEpicSubscriptionMutation,
},
};
+
+export default {
+ BoardType,
+ ListType,
+};
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 66580bdd30f..c6040f1e4aa 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -28,6 +28,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) {
const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
+ // TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274
+ // here we are using "window.location.search" as a temporary store
+ // only to unpack the params and do another validation inside
+ // 'performSearch' and 'setFilter' vuex actions.
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
index 80a37c9943d..3218c06357c 100644
--- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql
@@ -2,7 +2,7 @@
query getGroupProjects($fullPath: ID!, $search: String, $after: String) {
group(fullPath: $fullPath) {
- projects(search: $search, after: $after, first: 100) {
+ projects(search: $search, after: $after, first: 100, includeSubgroups: true) {
nodes {
id
name
diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 7ecf9261214..47ecb55c72b 100644
--- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -13,7 +13,6 @@ fragment IssueNode on Issue {
emailsDisabled
confidential
webUrl
- subscribed
relativePosition
milestone {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index e3f9d2f24c2..1888645ef78 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,3 +1,4 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
@@ -35,13 +36,27 @@ import {
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
+import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
+import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ assumeImmutableResults: true,
+ },
+ ),
});
let issueBoardsApp;
@@ -82,10 +97,14 @@ export default () => {
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
canAdminList: parseBoolean($boardApp.dataset.canAdminList),
- labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
+ multipleAssigneesFeatureAvailable: parseBoolean(
+ $boardApp.dataset.multipleAssigneesFeatureAvailable,
+ ),
+ epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable),
+ iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
boardWeight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
@@ -121,6 +140,7 @@ export default () => {
created() {
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
+ fullBoardId: fullBoardId($boardApp.dataset.boardId),
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 8005414962c..5158e82c320 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,8 +1,4 @@
import * as Sentry from '@sentry/browser';
-import { pick } from 'lodash';
-import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
-import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
-import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import {
BoardType,
ListType,
@@ -11,7 +7,14 @@ import {
ISSUABLE,
titleQueries,
subscriptionQueries,
-} from '~/boards/constants';
+ SupportedFilters,
+ deleteListQueries,
+ listsQuery,
+ updateListQueries,
+ issuableTypes,
+} from 'ee_else_ce/boards/constants';
+import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
+import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
@@ -19,7 +22,6 @@ import { s__ } from '~/locale';
import {
formatBoardLists,
formatListIssues,
- fullBoardId,
formatListsPageInfo,
formatIssue,
formatIssueInput,
@@ -27,10 +29,9 @@ import {
transformNotFilters,
moveItemListHelper,
getMoveData,
+ getSupportedParams,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
-import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
@@ -39,11 +40,6 @@ import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.g
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
-const notImplemented = () => {
- /* eslint-disable-next-line @gitlab/require-i18n-strings */
- throw new Error('Not implemented!');
-};
-
export const gqlClient = createGqClient(
{},
{
@@ -65,16 +61,11 @@ export default {
},
setFilters: ({ commit }, filters) => {
- const filterParams = pick(filters, [
- 'assigneeUsername',
- 'authorUsername',
- 'labelName',
- 'milestoneTitle',
- 'releaseTag',
- 'search',
- 'myReactionEmoji',
- ]);
- filterParams.not = transformNotFilters(filters);
+ const filterParams = {
+ ...getSupportedParams(filters, SupportedFilters),
+ not: transformNotFilters(filters),
+ };
+
commit(types.SET_FILTERS, filterParams);
},
@@ -90,24 +81,22 @@ export default {
}
},
- fetchLists: ({ dispatch }) => {
- dispatch('fetchIssueLists');
- },
-
- fetchIssueLists: ({ commit, state, dispatch }) => {
- const { boardType, filterParams, fullPath, boardId } = state;
+ fetchLists: ({ commit, state, dispatch }) => {
+ const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state;
const variables = {
fullPath,
- boardId: fullBoardId(boardId),
+ boardId: fullBoardId,
filters: filterParams,
- isGroup: boardType === BoardType.group,
- isProject: boardType === BoardType.project,
+ ...(issuableType === issuableTypes.issue && {
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
+ }),
};
return gqlClient
.query({
- query: boardListsQuery,
+ query: listsQuery[issuableType].query,
variables,
})
.then(({ data }) => {
@@ -141,7 +130,7 @@ export default {
{ state, commit, dispatch, getters },
{ backlog, labelId, milestoneId, assigneeId, iterationId },
) => {
- const { boardId } = state;
+ const { fullBoardId } = state;
const existingList = getters.getListByLabelId(labelId);
@@ -154,7 +143,7 @@ export default {
.mutate({
mutation: createBoardListMutation,
variables: {
- boardId: fullBoardId(boardId),
+ boardId: fullBoardId,
backlog,
labelId,
milestoneId,
@@ -242,10 +231,13 @@ export default {
dispatch('updateList', { listId, position: newPosition, backupList });
},
- updateList: ({ commit }, { listId, position, collapsed, backupList }) => {
+ updateList: (
+ { commit, state: { issuableType } },
+ { listId, position, collapsed, backupList },
+ ) => {
gqlClient
.mutate({
- mutation: updateBoardListMutation,
+ mutation: updateListQueries[issuableType].mutation,
variables: {
listId,
position,
@@ -266,14 +258,14 @@ export default {
commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
},
- removeList: ({ state, commit }, listId) => {
- const listsBackup = { ...state.boardLists };
+ removeList: ({ state: { issuableType, boardLists }, commit }, listId) => {
+ const listsBackup = { ...boardLists };
commit(types.REMOVE_LIST, listId);
return gqlClient
.mutate({
- mutation: destroyBoardListMutation,
+ mutation: deleteListQueries[issuableType].mutation,
variables: {
listId,
},
@@ -297,11 +289,11 @@ export default {
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
- const { fullPath, boardId, boardType, filterParams } = state;
+ const { fullPath, fullBoardId, boardType, filterParams } = state;
const variables = {
fullPath,
- boardId: fullBoardId(boardId),
+ boardId: fullBoardId,
id: listId,
filters: filterParams,
isGroup: boardType === BoardType.group,
@@ -430,7 +422,7 @@ export default {
try {
const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
const {
- boardId,
+ fullBoardId,
boardItems: {
[itemId]: { iid, referencePath },
},
@@ -441,7 +433,7 @@ export default {
variables: {
iid,
projectPath: referencePath.split(/[#]/)[0],
- boardId: fullBoardId(boardId),
+ boardId: fullBoardId,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
@@ -653,6 +645,15 @@ export default {
});
},
+ setActiveItemConfidential: ({ commit, getters }, confidential) => {
+ const { activeBoardItem } = getters;
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
+ prop: 'confidential',
+ value: confidential,
+ });
+ },
+
fetchGroupProjects: ({ commit, state }, { search = '', fetchNext = false }) => {
commit(types.REQUEST_GROUP_PROJECTS, fetchNext);
@@ -731,28 +732,4 @@ export default {
unsetError: ({ commit }) => {
commit(types.SET_ERROR, undefined);
},
-
- fetchBacklog: () => {
- notImplemented();
- },
-
- bulkUpdateIssues: () => {
- notImplemented();
- },
-
- fetchIssue: () => {
- notImplemented();
- },
-
- toggleIssueSubscription: () => {
- notImplemented();
- },
-
- showPage: () => {
- notImplemented();
- },
-
- toggleEmptyState: () => {
- notImplemented();
- },
};
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 0589851c658..b61ecc5ccb6 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -21,7 +21,7 @@ export default {
groupPathForActiveIssue: (_, getters) => {
const { referencePath = '' } = getters.activeBoardItem;
- return referencePath.slice(0, referencePath.indexOf('/'));
+ return referencePath.slice(0, referencePath.lastIndexOf('/'));
},
projectPathForActiveIssue: (_, getters) => {
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 22b9905ee62..ccea2917c2c 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -9,9 +9,7 @@ export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
-export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
-export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
export const MOVE_LIST = 'MOVE_LIST';
export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
@@ -20,19 +18,11 @@ export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
-export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
-export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
-export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
-export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
-export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
-export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
-export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
-export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 561c21b78c1..667628b2998 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -6,11 +6,6 @@ import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
-const notImplemented = () => {
- /* eslint-disable-next-line @gitlab/require-i18n-strings */
- throw new Error('Not implemented!');
-};
-
const updateListItemsCount = ({ state, listId, value }) => {
const list = state.boardLists[listId];
if (state.issuableType === issuableTypes.epic) {
@@ -40,8 +35,9 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const { boardType, disabled, boardId, fullPath, boardConfig, issuableType } = data;
+ const { boardType, disabled, boardId, fullBoardId, fullPath, boardConfig, issuableType } = data;
state.boardId = boardId;
+ state.fullBoardId = fullBoardId;
state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
@@ -93,18 +89,10 @@ export default {
state.error = s__('Boards|An error occurred while generating lists. Please reload the page.');
},
- [mutationTypes.REQUEST_ADD_LIST]: () => {
- notImplemented();
- },
-
[mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => {
Vue.set(state.boardLists, list.id, list);
},
- [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => {
- notImplemented();
- },
-
[mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
const { boardLists } = state;
Vue.set(boardLists, movedList.id, movedList);
@@ -171,35 +159,11 @@ export default {
state.isSettingAssignees = isLoading;
},
- [mutationTypes.REQUEST_ADD_ISSUE]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_ADD_ISSUE_SUCCESS]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_ADD_ISSUE_ERROR]: () => {
- notImplemented();
- },
-
[mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => {
const issueId = getIdFromGraphQLId(issue.id);
Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId }));
},
- [mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_UPDATE_ISSUE_SUCCESS]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_UPDATE_ISSUE_ERROR]: () => {
- notImplemented();
- },
-
[mutationTypes.ADD_BOARD_ITEM_TO_LIST]: (
state,
{ itemId, listId, moveBeforeId, moveAfterId, atIndex },
@@ -219,14 +183,6 @@ export default {
Vue.delete(state.boardItems, itemId);
},
- [mutationTypes.SET_CURRENT_PAGE]: () => {
- notImplemented();
- },
-
- [mutationTypes.TOGGLE_EMPTY_STATE]: () => {
- notImplemented();
- },
-
[mutationTypes.REQUEST_GROUP_PROJECTS]: (state, fetchNext) => {
Vue.set(state, 'groupProjectsFlags', {
[fetchNext ? 'isLoadingMore' : 'isLoading']: true,
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index e5923124653..b959d97daea 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -162,23 +162,26 @@ export default {
</p>
</template>
</gl-table>
- <div class="ci-variable-actions" :class="{ 'justify-content-center': !tableIsNotEmpty }">
+ <div
+ class="ci-variable-actions gl-display-flex"
+ :class="{ 'justify-content-center': !tableIsNotEmpty }"
+ >
+ <gl-button
+ ref="add-ci-variable"
+ v-gl-modal-directive="$options.modalId"
+ class="gl-mr-3"
+ data-qa-selector="add_ci_variable_button"
+ variant="confirm"
+ category="primary"
+ >{{ __('Add variable') }}</gl-button
+ >
<gl-button
v-if="tableIsNotEmpty"
ref="secret-value-reveal-button"
data-qa-selector="reveal_ci_variable_value_button"
- class="gl-mr-3"
@click="toggleValues(!valuesHidden)"
>{{ valuesButtonText }}</gl-button
>
- <gl-button
- ref="add-ci-variable"
- v-gl-modal-directive="$options.modalId"
- data-qa-selector="add_ci_variable_button"
- variant="success"
- category="primary"
- >{{ __('Add Variable') }}</gl-button
- >
</div>
</div>
</template>
diff --git a/app/assets/javascripts/code_quality_walkthrough/components/step.vue b/app/assets/javascripts/code_quality_walkthrough/components/step.vue
new file mode 100644
index 00000000000..1a23c96b7d6
--- /dev/null
+++ b/app/assets/javascripts/code_quality_walkthrough/components/step.vue
@@ -0,0 +1,150 @@
+<script>
+import { GlPopover, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
+import { STEPS, STEPSTATES } from '../constants';
+import {
+ isWalkthroughEnabled,
+ getExperimentSettings,
+ setExperimentSettings,
+ track,
+} from '../utils';
+
+export default {
+ target: '#js-code-quality-walkthrough',
+ components: {
+ GlPopover,
+ GlSprintf,
+ GlButton,
+ GlAlert,
+ },
+ props: {
+ step: {
+ type: String,
+ required: true,
+ },
+ link: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ dismissedSettings: getExperimentSettings(),
+ currentStep: STEPSTATES[this.step],
+ };
+ },
+ computed: {
+ isPopoverVisible() {
+ return (
+ [
+ STEPS.commitCiFile,
+ STEPS.runningPipeline,
+ STEPS.successPipeline,
+ STEPS.failedPipeline,
+ ].includes(this.step) &&
+ isWalkthroughEnabled() &&
+ !this.isDismissed
+ );
+ },
+ isAlertVisible() {
+ return this.step === STEPS.troubleshootJob && isWalkthroughEnabled() && !this.isDismissed;
+ },
+ isDismissed() {
+ return this.dismissedSettings[this.step];
+ },
+ title() {
+ return this.currentStep?.title || '';
+ },
+ body() {
+ return this.currentStep?.body || '';
+ },
+ buttonText() {
+ return this.currentStep?.buttonText || '';
+ },
+ buttonLink() {
+ return [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step) ? this.link : '';
+ },
+ placement() {
+ return this.currentStep?.placement || 'bottom';
+ },
+ offset() {
+ return this.currentStep?.offset || 0;
+ },
+ },
+ created() {
+ this.trackDisplayed();
+ },
+ updated() {
+ this.trackDisplayed();
+ },
+ methods: {
+ onDismiss() {
+ this.$set(this.dismissedSettings, this.step, true);
+ setExperimentSettings(this.dismissedSettings);
+ const action = [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step)
+ ? 'view_logs'
+ : 'dismissed';
+ this.trackAction(action);
+ },
+ trackDisplayed() {
+ if (this.isPopoverVisible || this.isAlertVisible) {
+ this.trackAction('displayed');
+ }
+ },
+ trackAction(action) {
+ track(`${this.step}_${action}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-popover
+ v-if="isPopoverVisible"
+ :key="step"
+ :target="$options.target"
+ :placement="placement"
+ :offset="offset"
+ show
+ triggers="manual"
+ container="viewport"
+ >
+ <template #title>
+ <gl-sprintf :message="title">
+ <template #emoji="{ content }">
+ <gl-emoji class="gl-mr-2" :data-name="content"
+ /></template>
+ </gl-sprintf>
+ </template>
+ <gl-sprintf :message="body">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #lineBreak>
+ <div class="gl-mt-5"></div>
+ </template>
+ <template #emoji="{ content }">
+ <gl-emoji :data-name="content" />
+ </template>
+ </gl-sprintf>
+ <div class="gl-mt-2 gl-text-right">
+ <gl-button category="tertiary" variant="link" :href="buttonLink" @click="onDismiss">
+ {{ buttonText }}
+ </gl-button>
+ </div>
+ </gl-popover>
+ <gl-alert
+ v-if="isAlertVisible"
+ variant="tip"
+ :title="title"
+ :primary-button-text="buttonText"
+ :primary-button-link="link"
+ class="gl-my-5"
+ @primaryAction="trackAction('clicked')"
+ @dismiss="onDismiss"
+ >
+ {{ body }}
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/code_quality_walkthrough/constants.js b/app/assets/javascripts/code_quality_walkthrough/constants.js
new file mode 100644
index 00000000000..011df06b5cc
--- /dev/null
+++ b/app/assets/javascripts/code_quality_walkthrough/constants.js
@@ -0,0 +1,67 @@
+import { s__ } from '~/locale';
+
+export const EXPERIMENT_NAME = 'code_quality_walkthrough';
+
+export const STEPS = {
+ commitCiFile: 'commit_ci_file',
+ runningPipeline: 'running_pipeline',
+ successPipeline: 'success_pipeline',
+ failedPipeline: 'failed_pipeline',
+ troubleshootJob: 'troubleshoot_job',
+};
+
+export const STEPSTATES = {
+ [STEPS.commitCiFile]: {
+ title: s__("codeQualityWalkthrough|Let's start by creating a new CI file."),
+ body: s__(
+ 'codeQualityWalkthrough|To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page.',
+ ),
+ buttonText: s__('codeQualityWalkthrough|Got it'),
+ placement: 'right',
+ offset: 90,
+ },
+ [STEPS.runningPipeline]: {
+ title: s__(
+ 'codeQualityWalkthrough|Congrats! Your first pipeline is running %{emojiStart}zap%{emojiEnd}',
+ ),
+ body: s__(
+ "codeQualityWalkthrough|Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!",
+ ),
+ buttonText: s__('codeQualityWalkthrough|Got it'),
+ offset: 97,
+ },
+ [STEPS.successPipeline]: {
+ title: s__(
+ "codeQualityWalkthrough|Well done! You've just automated your code quality review. %{emojiStart}raised_hands%{emojiEnd}",
+ ),
+ body: s__(
+ 'codeQualityWalkthrough|A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs.',
+ ),
+ buttonText: s__('codeQualityWalkthrough|View the logs'),
+ offset: 98,
+ },
+ [STEPS.failedPipeline]: {
+ title: s__(
+ "codeQualityWalkthrough|Something went wrong. %{emojiStart}thinking%{emojiEnd} Let's fix it.",
+ ),
+ body: s__(
+ "codeQualityWalkthrough|Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it.",
+ ),
+ buttonText: s__('codeQualityWalkthrough|View the logs'),
+ offset: 98,
+ },
+ [STEPS.troubleshootJob]: {
+ title: s__('codeQualityWalkthrough|Troubleshoot your code quality job'),
+ body: s__(
+ 'codeQualityWalkthrough|Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation.',
+ ),
+ buttonText: s__('codeQualityWalkthrough|Read the documentation'),
+ },
+};
+
+export const PIPELINE_STATUSES = {
+ running: 'running',
+ successWithWarnings: 'success-with-warnings',
+ success: 'success',
+ failed: 'failed',
+};
diff --git a/app/assets/javascripts/code_quality_walkthrough/index.js b/app/assets/javascripts/code_quality_walkthrough/index.js
new file mode 100644
index 00000000000..b0592b8a84b
--- /dev/null
+++ b/app/assets/javascripts/code_quality_walkthrough/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Step from './components/step.vue';
+
+export default (el) =>
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(Step, {
+ props: {
+ step: el.dataset.step,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/code_quality_walkthrough/utils.js b/app/assets/javascripts/code_quality_walkthrough/utils.js
new file mode 100644
index 00000000000..97c80f6eff7
--- /dev/null
+++ b/app/assets/javascripts/code_quality_walkthrough/utils.js
@@ -0,0 +1,38 @@
+import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
+import { getExperimentData } from '~/experimentation/utils';
+import { setCookie, getCookie, getParameterByName } from '~/lib/utils/common_utils';
+import Tracking from '~/tracking';
+import { EXPERIMENT_NAME } from './constants';
+
+export function getExperimentSettings() {
+ return JSON.parse(getCookie(EXPERIMENT_NAME) || '{}');
+}
+
+export function setExperimentSettings(settings) {
+ setCookie(EXPERIMENT_NAME, settings);
+}
+
+export function isWalkthroughEnabled() {
+ return getParameterByName(EXPERIMENT_NAME);
+}
+
+export function track(action) {
+ const { data } = getExperimentSettings();
+
+ if (data) {
+ Tracking.event(EXPERIMENT_NAME, action, {
+ context: {
+ schema: TRACKING_CONTEXT_SCHEMA,
+ data,
+ },
+ });
+ }
+}
+
+export function startCodeQualityWalkthrough() {
+ const data = getExperimentData(EXPERIMENT_NAME);
+
+ if (data) {
+ setExperimentSettings({ data });
+ }
+}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 6f496ffc6ae..29ad6cc4125 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -27,6 +27,10 @@ export default () => {
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new Vue({
+ provide: {
+ artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
+ artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
+ },
render(createElement) {
return createElement(CommitPipelinesTable, {
props: {
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 839d4de912d..7896268acf0 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,18 +1,24 @@
<script>
-import { EditorContent } from 'tiptap';
-import createEditor from '../services/create_editor';
+import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
+import { ContentEditor } from '../services/content_editor';
+import TopToolbar from './top_toolbar.vue';
export default {
components: {
- EditorContent,
+ TiptapEditorContent,
+ TopToolbar,
},
- data() {
- return {
- editor: createEditor(),
- };
+ props: {
+ contentEditor: {
+ type: ContentEditor,
+ required: true,
+ },
},
};
</script>
<template>
- <editor-content :editor="editor" />
+ <div class="md md-area" :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }">
+ <top-toolbar class="gl-mb-4" :content-editor="contentEditor" />
+ <tiptap-editor-content :editor="contentEditor.tiptapEditor" />
+ </div>
</template>
diff --git a/app/assets/javascripts/content_editor/components/divider.vue b/app/assets/javascripts/content_editor/components/divider.vue
new file mode 100644
index 00000000000..b77bd7b7cf3
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/divider.vue
@@ -0,0 +1,3 @@
+<template>
+ <span class="gl-mx-3 gl-border-r-solid gl-border-r-1 gl-border-gray-200"></span>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
new file mode 100644
index 00000000000..0af12812f3b
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { Editor as TiptapEditor } from '@tiptap/vue-2';
+
+export default {
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ iconName: {
+ type: String,
+ required: true,
+ },
+ tiptapEditor: {
+ type: TiptapEditor,
+ required: true,
+ },
+ contentType: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ editorCommand: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isActive() {
+ return this.tiptapEditor.isActive(this.contentType) && this.tiptapEditor.isFocused;
+ },
+ },
+ methods: {
+ execute() {
+ const { contentType } = this;
+
+ if (this.editorCommand) {
+ this.tiptapEditor.chain()[this.editorCommand]().focus().run();
+ }
+
+ this.$emit('execute', { contentType });
+ },
+ },
+};
+</script>
+<template>
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ size="small"
+ class="gl-mx-2"
+ :class="{ active: isActive }"
+ :aria-label="label"
+ :title="label"
+ :icon="iconName"
+ @click="execute"
+ />
+</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
new file mode 100644
index 00000000000..b18649d4e57
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -0,0 +1,94 @@
+<script>
+import Tracking from '~/tracking';
+import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
+import { ContentEditor } from '../services/content_editor';
+import Divider from './divider.vue';
+import ToolbarButton from './toolbar_button.vue';
+
+const trackingMixin = Tracking.mixin({
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+});
+
+export default {
+ components: {
+ ToolbarButton,
+ Divider,
+ },
+ mixins: [trackingMixin],
+ props: {
+ contentEditor: {
+ type: ContentEditor,
+ required: true,
+ },
+ },
+ methods: {
+ trackToolbarControlExecution({ contentType: property, value }) {
+ this.track(TOOLBAR_CONTROL_TRACKING_ACTION, {
+ property,
+ value,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-display-flex gl-justify-content-end gl-pb-3 gl-pt-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
+ >
+ <toolbar-button
+ data-testid="bold"
+ content-type="bold"
+ icon-name="bold"
+ editor-command="toggleBold"
+ :label="__('Bold text')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="italic"
+ content-type="italic"
+ icon-name="italic"
+ editor-command="toggleItalic"
+ :label="__('Italic text')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="code"
+ content-type="code"
+ icon-name="code"
+ editor-command="toggleCode"
+ :label="__('Code')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <divider />
+ <toolbar-button
+ data-testid="blockquote"
+ content-type="blockquote"
+ icon-name="quote"
+ editor-command="toggleBlockquote"
+ :label="__('Insert a quote')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="bullet-list"
+ content-type="bulletList"
+ icon-name="list-bulleted"
+ editor-command="toggleBulletList"
+ :label="__('Add a bullet list')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="ordered-list"
+ content-type="orderedList"
+ icon-name="list-numbered"
+ editor-command="toggleOrderedList"
+ :label="__('Add a numbered list')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index eb6deff434d..45ebd87dac9 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -3,3 +3,8 @@ import { s__ } from '~/locale';
export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__(
'ContentEditor|You have to provide a renderMarkdown function or a custom serializer',
);
+
+export const CONTENT_EDITOR_TRACKING_LABEL = 'content_editor';
+export const TOOLBAR_CONTROL_TRACKING_ACTION = 'execute_toolbar_control';
+export const KEYBOARD_SHORTCUT_TRACKING_ACTION = 'execute_keyboard_shortcut';
+export const INPUT_RULE_TRACKING_ACTION = 'execute_input_rule';
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
new file mode 100644
index 00000000000..a4297b4550c
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -0,0 +1,5 @@
+import { Blockquote } from '@tiptap/extension-blockquote';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Blockquote;
+export const serializer = defaultMarkdownSerializer.nodes.blockquote;
diff --git a/app/assets/javascripts/content_editor/extensions/bold.js b/app/assets/javascripts/content_editor/extensions/bold.js
new file mode 100644
index 00000000000..e90e7b59da0
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/bold.js
@@ -0,0 +1,5 @@
+import { Bold } from '@tiptap/extension-bold';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Bold;
+export const serializer = defaultMarkdownSerializer.marks.strong;
diff --git a/app/assets/javascripts/content_editor/extensions/bullet_list.js b/app/assets/javascripts/content_editor/extensions/bullet_list.js
new file mode 100644
index 00000000000..178b798e2d4
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/bullet_list.js
@@ -0,0 +1,5 @@
+import { BulletList } from '@tiptap/extension-bullet-list';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = BulletList;
+export const serializer = defaultMarkdownSerializer.nodes.bullet_list;
diff --git a/app/assets/javascripts/content_editor/extensions/code.js b/app/assets/javascripts/content_editor/extensions/code.js
new file mode 100644
index 00000000000..8be50dc39c5
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/code.js
@@ -0,0 +1,5 @@
+import { Code } from '@tiptap/extension-code';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Code;
+export const serializer = defaultMarkdownSerializer.marks.code;
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 1d050ed208b..ce8bd57c7e3 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,38 +1,27 @@
-import { CodeBlockHighlight as BaseCodeBlockHighlight } from 'tiptap-extensions';
+import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-export default class GlCodeBlockHighlight extends BaseCodeBlockHighlight {
- get schema() {
- const baseSchema = super.schema;
+const extractLanguage = (element) => element.firstElementChild?.getAttribute('lang');
+const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({
+ addAttributes() {
return {
- ...baseSchema,
- attrs: {
- params: {
- default: null,
+ ...this.parent(),
+ /* `params` is the name of the attribute that
+ prosemirror-markdown uses to extract the language
+ of a codeblock.
+ https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62
+ */
+ params: {
+ parseHTML: (element) => {
+ return {
+ params: extractLanguage(element),
+ };
},
},
- parseDOM: [
- {
- tag: 'pre',
- preserveWhitespace: 'full',
- getAttrs: (node) => {
- const code = node.querySelector('code');
-
- if (!code) {
- return null;
- }
-
- return {
- /* `params` is the name of the attribute that
- prosemirror-markdown uses to extract the language
- of a codeblock.
- https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.js#L62
- */
- params: code.getAttribute('lang'),
- };
- },
- },
- ],
};
- }
-}
+ },
+});
+
+export const tiptapExtension = ExtendedCodeBlockLowlight;
+export const serializer = defaultMarkdownSerializer.nodes.code_block;
diff --git a/app/assets/javascripts/content_editor/extensions/document.js b/app/assets/javascripts/content_editor/extensions/document.js
new file mode 100644
index 00000000000..99aa8d6235a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/document.js
@@ -0,0 +1,3 @@
+import Document from '@tiptap/extension-document';
+
+export const tiptapExtension = Document;
diff --git a/app/assets/javascripts/content_editor/extensions/dropcursor.js b/app/assets/javascripts/content_editor/extensions/dropcursor.js
new file mode 100644
index 00000000000..44c378ac7db
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/dropcursor.js
@@ -0,0 +1,3 @@
+import Dropcursor from '@tiptap/extension-dropcursor';
+
+export const tiptapExtension = Dropcursor;
diff --git a/app/assets/javascripts/content_editor/extensions/gapcursor.js b/app/assets/javascripts/content_editor/extensions/gapcursor.js
new file mode 100644
index 00000000000..2db862e4580
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/gapcursor.js
@@ -0,0 +1,3 @@
+import Gapcursor from '@tiptap/extension-gapcursor';
+
+export const tiptapExtension = Gapcursor;
diff --git a/app/assets/javascripts/content_editor/extensions/hard_break.js b/app/assets/javascripts/content_editor/extensions/hard_break.js
new file mode 100644
index 00000000000..dc1ba431151
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/hard_break.js
@@ -0,0 +1,5 @@
+import { HardBreak } from '@tiptap/extension-hard-break';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = HardBreak;
+export const serializer = defaultMarkdownSerializer.nodes.hard_break;
diff --git a/app/assets/javascripts/content_editor/extensions/heading.js b/app/assets/javascripts/content_editor/extensions/heading.js
new file mode 100644
index 00000000000..f69869d1e09
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/heading.js
@@ -0,0 +1,5 @@
+import { Heading } from '@tiptap/extension-heading';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Heading;
+export const serializer = defaultMarkdownSerializer.nodes.heading;
diff --git a/app/assets/javascripts/content_editor/extensions/history.js b/app/assets/javascripts/content_editor/extensions/history.js
new file mode 100644
index 00000000000..554d797d30a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/history.js
@@ -0,0 +1,3 @@
+import History from '@tiptap/extension-history';
+
+export const tiptapExtension = History;
diff --git a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
new file mode 100644
index 00000000000..dcc59476518
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
@@ -0,0 +1,5 @@
+import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = HorizontalRule;
+export const serializer = defaultMarkdownSerializer.nodes.horizontal_rule;
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
new file mode 100644
index 00000000000..4f0109fd751
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -0,0 +1,9 @@
+import { Image } from '@tiptap/extension-image';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+const ExtendedImage = Image.extend({
+ defaultOptions: { inline: true },
+});
+
+export const tiptapExtension = ExtendedImage;
+export const serializer = defaultMarkdownSerializer.nodes.image;
diff --git a/app/assets/javascripts/content_editor/extensions/italic.js b/app/assets/javascripts/content_editor/extensions/italic.js
new file mode 100644
index 00000000000..b8a7c4aba3e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/italic.js
@@ -0,0 +1,4 @@
+import { Italic } from '@tiptap/extension-italic';
+
+export const tiptapExtension = Italic;
+export const serializer = { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true };
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
new file mode 100644
index 00000000000..9a2fa7a5c98
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -0,0 +1,5 @@
+import { Link } from '@tiptap/extension-link';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Link;
+export const serializer = defaultMarkdownSerializer.marks.link;
diff --git a/app/assets/javascripts/content_editor/extensions/list_item.js b/app/assets/javascripts/content_editor/extensions/list_item.js
new file mode 100644
index 00000000000..86da98f6df7
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/list_item.js
@@ -0,0 +1,5 @@
+import { ListItem } from '@tiptap/extension-list-item';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = ListItem;
+export const serializer = defaultMarkdownSerializer.nodes.list_item;
diff --git a/app/assets/javascripts/content_editor/extensions/ordered_list.js b/app/assets/javascripts/content_editor/extensions/ordered_list.js
new file mode 100644
index 00000000000..d980ab8bf10
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/ordered_list.js
@@ -0,0 +1,5 @@
+import { OrderedList } from '@tiptap/extension-ordered-list';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = OrderedList;
+export const serializer = defaultMarkdownSerializer.nodes.ordered_list;
diff --git a/app/assets/javascripts/content_editor/extensions/paragraph.js b/app/assets/javascripts/content_editor/extensions/paragraph.js
new file mode 100644
index 00000000000..6c9f204b8ac
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/paragraph.js
@@ -0,0 +1,5 @@
+import { Paragraph } from '@tiptap/extension-paragraph';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Paragraph;
+export const serializer = defaultMarkdownSerializer.nodes.paragraph;
diff --git a/app/assets/javascripts/content_editor/extensions/text.js b/app/assets/javascripts/content_editor/extensions/text.js
new file mode 100644
index 00000000000..0d76aa1f1a7
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/text.js
@@ -0,0 +1,5 @@
+import { Text } from '@tiptap/extension-text';
+import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+
+export const tiptapExtension = Text;
+export const serializer = defaultMarkdownSerializer.nodes.text;
diff --git a/app/assets/javascripts/content_editor/index.js b/app/assets/javascripts/content_editor/index.js
index e6ef3965da1..2a7dc9b713d 100644
--- a/app/assets/javascripts/content_editor/index.js
+++ b/app/assets/javascripts/content_editor/index.js
@@ -1,2 +1,2 @@
-export { default as createEditor } from './services/create_editor';
+export * from './services/create_content_editor';
export { default as ContentEditor } from './components/content_editor.vue';
diff --git a/app/assets/javascripts/content_editor/services/build_serializer_config.js b/app/assets/javascripts/content_editor/services/build_serializer_config.js
new file mode 100644
index 00000000000..75e2b0f9eba
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/build_serializer_config.js
@@ -0,0 +1,22 @@
+const buildSerializerConfig = (extensions = []) =>
+ extensions
+ .filter(({ serializer }) => serializer)
+ .reduce(
+ (serializers, { serializer, tiptapExtension: { name, type } }) => {
+ const collection = `${type}s`;
+
+ return {
+ ...serializers,
+ [collection]: {
+ ...serializers[collection],
+ [name]: serializer,
+ },
+ };
+ },
+ {
+ nodes: {},
+ marks: {},
+ },
+ );
+
+export default buildSerializerConfig;
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
new file mode 100644
index 00000000000..e2188f5aa69
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -0,0 +1,25 @@
+/* eslint-disable no-underscore-dangle */
+export class ContentEditor {
+ constructor({ tiptapEditor, serializer }) {
+ this._tiptapEditor = tiptapEditor;
+ this._serializer = serializer;
+ }
+
+ get tiptapEditor() {
+ return this._tiptapEditor;
+ }
+
+ async setSerializedContent(serializedContent) {
+ const { _tiptapEditor: editor, _serializer: serializer } = this;
+
+ editor.commands.setContent(
+ await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
+ );
+ }
+
+ getSerializedContent() {
+ const { _tiptapEditor: editor, _serializer: serializer } = this;
+
+ return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
+ }
+}
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
new file mode 100644
index 00000000000..df45287e6cb
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -0,0 +1,76 @@
+import { Editor } from '@tiptap/vue-2';
+import { isFunction } from 'lodash';
+import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
+import * as Blockquote from '../extensions/blockquote';
+import * as Bold from '../extensions/bold';
+import * as BulletList from '../extensions/bullet_list';
+import * as Code from '../extensions/code';
+import * as CodeBlockHighlight from '../extensions/code_block_highlight';
+import * as Document from '../extensions/document';
+import * as Dropcursor from '../extensions/dropcursor';
+import * as Gapcursor from '../extensions/gapcursor';
+import * as HardBreak from '../extensions/hard_break';
+import * as Heading from '../extensions/heading';
+import * as History from '../extensions/history';
+import * as HorizontalRule from '../extensions/horizontal_rule';
+import * as Image from '../extensions/image';
+import * as Italic from '../extensions/italic';
+import * as Link from '../extensions/link';
+import * as ListItem from '../extensions/list_item';
+import * as OrderedList from '../extensions/ordered_list';
+import * as Paragraph from '../extensions/paragraph';
+import * as Text from '../extensions/text';
+import buildSerializerConfig from './build_serializer_config';
+import { ContentEditor } from './content_editor';
+import createMarkdownSerializer from './markdown_serializer';
+import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
+
+const builtInContentEditorExtensions = [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ Document,
+ Dropcursor,
+ Gapcursor,
+ HardBreak,
+ Heading,
+ History,
+ HorizontalRule,
+ Image,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Paragraph,
+ Text,
+];
+
+const collectTiptapExtensions = (extensions = []) =>
+ extensions.map(({ tiptapExtension }) => tiptapExtension);
+
+const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
+ new Editor({
+ extensions: [...extensions],
+ editorProps: {
+ attributes: {
+ class: 'gl-outline-0!',
+ },
+ },
+ ...options,
+ });
+
+export const createContentEditor = ({ renderMarkdown, extensions = [], tiptapOptions } = {}) => {
+ if (!isFunction(renderMarkdown)) {
+ throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
+ }
+
+ const allExtensions = [...builtInContentEditorExtensions, ...extensions];
+ const tiptapExtensions = collectTiptapExtensions(allExtensions).map(trackInputRulesAndShortcuts);
+ const tiptapEditor = createTiptapEditor({ extensions: tiptapExtensions, ...tiptapOptions });
+ const serializerConfig = buildSerializerConfig(allExtensions);
+ const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
+
+ return new ContentEditor({ tiptapEditor, serializer });
+};
diff --git a/app/assets/javascripts/content_editor/services/create_editor.js b/app/assets/javascripts/content_editor/services/create_editor.js
deleted file mode 100644
index 128d332b0a2..00000000000
--- a/app/assets/javascripts/content_editor/services/create_editor.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { isFunction, isString } from 'lodash';
-import { Editor } from 'tiptap';
-import {
- Bold,
- Italic,
- Code,
- Link,
- Image,
- Heading,
- Blockquote,
- HorizontalRule,
- BulletList,
- OrderedList,
- ListItem,
-} from 'tiptap-extensions';
-import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
-import CodeBlockHighlight from '../extensions/code_block_highlight';
-import createMarkdownSerializer from './markdown_serializer';
-
-const createEditor = async ({ content, renderMarkdown, serializer: customSerializer } = {}) => {
- if (!customSerializer && !isFunction(renderMarkdown)) {
- throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
- }
-
- const editor = new Editor({
- extensions: [
- new Bold(),
- new Italic(),
- new Code(),
- new Link(),
- new Image(),
- new Heading({ levels: [1, 2, 3, 4, 5, 6] }),
- new Blockquote(),
- new HorizontalRule(),
- new BulletList(),
- new ListItem(),
- new OrderedList(),
- new CodeBlockHighlight(),
- ],
- });
- const serializer = customSerializer || createMarkdownSerializer({ render: renderMarkdown });
-
- editor.setSerializedContent = async (serializedContent) => {
- editor.setContent(
- await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
- );
- };
-
- editor.getSerializedContent = () => {
- return serializer.serialize({ schema: editor.schema, content: editor.getJSON() });
- };
-
- if (isString(content)) {
- await editor.setSerializedContent(content);
- }
-
- return editor;
-};
-
-export default createEditor;
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index e3b5775e320..f121cc9affd 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -1,7 +1,4 @@
-import {
- MarkdownSerializer as ProseMirrorMarkdownSerializer,
- defaultMarkdownSerializer,
-} from 'prosemirror-markdown';
+import { MarkdownSerializer as ProseMirrorMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
@@ -18,56 +15,46 @@ const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
-const create = ({ render = () => null }) => {
- return {
- /**
- * Converts a Markdown string into a ProseMirror JSONDocument based
- * on a ProseMirror schema.
- * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
- * the types of content supported in the document
- * @param {String} params.content An arbitrary markdown string
- * @returns A ProseMirror JSONDocument
- */
- deserialize: async ({ schema, content }) => {
- const html = await render(content);
-
- if (!html) {
- return null;
- }
-
- const parser = new DOMParser();
- const {
- body: { firstElementChild },
- } = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
- const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
-
- return state.toJSON();
- },
-
- /**
- * Converts a ProseMirror JSONDocument based
- * on a ProseMirror schema into Markdown
- * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
- * the types of content supported in the document
- * @param {String} params.content A ProseMirror JSONDocument
- * @returns A Markdown string
- */
- serialize: ({ schema, content }) => {
- const document = schema.nodeFromJSON(content);
- const serializer = new ProseMirrorMarkdownSerializer(defaultMarkdownSerializer.nodes, {
- ...defaultMarkdownSerializer.marks,
- bold: {
- // creates a bold alias for the strong mark converter
- ...defaultMarkdownSerializer.marks.strong,
- },
- italic: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
- });
-
- return serializer.serialize(document, {
- tightLists: true,
- });
- },
- };
-};
-
-export default create;
+export default ({ render = () => null, serializerConfig }) => ({
+ /**
+ * Converts a Markdown string into a ProseMirror JSONDocument based
+ * on a ProseMirror schema.
+ * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
+ * the types of content supported in the document
+ * @param {String} params.content An arbitrary markdown string
+ * @returns A ProseMirror JSONDocument
+ */
+ deserialize: async ({ schema, content }) => {
+ const html = await render(content);
+
+ if (!html) {
+ return null;
+ }
+
+ const parser = new DOMParser();
+ const {
+ body: { firstElementChild },
+ } = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
+ const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
+
+ return state.toJSON();
+ },
+
+ /**
+ * Converts a ProseMirror JSONDocument based
+ * on a ProseMirror schema into Markdown
+ * @param {ProseMirror.Schema} params.schema A ProseMirror schema that defines
+ * the types of content supported in the document
+ * @param {String} params.content A ProseMirror JSONDocument
+ * @returns A Markdown string
+ */
+ serialize: ({ schema, content }) => {
+ const proseMirrorDocument = schema.nodeFromJSON(content);
+ const { nodes, marks } = serializerConfig;
+ const serializer = new ProseMirrorMarkdownSerializer(nodes, marks);
+
+ return serializer.serialize(proseMirrorDocument, {
+ tightLists: true,
+ });
+ },
+});
diff --git a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
new file mode 100644
index 00000000000..860e5372bc2
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
@@ -0,0 +1,61 @@
+import { mapValues, omit } from 'lodash';
+import { InputRule } from 'prosemirror-inputrules';
+import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
+import Tracking from '~/tracking';
+import {
+ CONTENT_EDITOR_TRACKING_LABEL,
+ KEYBOARD_SHORTCUT_TRACKING_ACTION,
+ INPUT_RULE_TRACKING_ACTION,
+} from '../constants';
+
+const trackKeyboardShortcut = (contentType, commandFn, shortcut) => () => {
+ Tracking.event(undefined, KEYBOARD_SHORTCUT_TRACKING_ACTION, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property: `${contentType}.${shortcut}`,
+ });
+ return commandFn();
+};
+
+const trackInputRule = (contentType, inputRule) => {
+ return new InputRule(inputRule.match, (...args) => {
+ const result = inputRule.handler(...args);
+
+ if (result) {
+ Tracking.event(undefined, INPUT_RULE_TRACKING_ACTION, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property: contentType,
+ });
+ }
+
+ return result;
+ });
+};
+
+const trackInputRulesAndShortcuts = (tiptapExtension) => {
+ return tiptapExtension.extend({
+ addKeyboardShortcuts() {
+ const shortcuts = this.parent?.() || {};
+ const { name } = this;
+
+ /**
+ * We don’t want to track keyboard shortcuts
+ * that are not deliberately executed to create
+ * new types of content
+ */
+ const withoutEnterShortcut = omit(shortcuts, [ENTER_KEY, BACKSPACE_KEY]);
+ const decorated = mapValues(withoutEnterShortcut, (commandFn, shortcut) =>
+ trackKeyboardShortcut(name, commandFn, shortcut),
+ );
+
+ return decorated;
+ },
+ addInputRules() {
+ const inputRules = this.parent?.() || [];
+ const { name } = this;
+
+ return inputRules.map((inputRule) => trackInputRule(name, inputRule));
+ },
+ });
+};
+
+export default trackInputRulesAndShortcuts;
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 000faacb7d7..1c0dab11392 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -42,7 +42,7 @@ export default class CreateMergeRequestDropdown {
this.refInput = this.wrapperEl.querySelector('.js-ref');
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
- this.unavailableButtonSpinner = this.unavailableButton.querySelector('.spinner');
+ this.unavailableButtonSpinner = this.unavailableButton.querySelector('.gl-spinner');
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
this.branchCreated = false;
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index df77d641e21..11a263015e4 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import banner from './banner.vue';
import stageCodeComponent from './stage_code_component.vue';
@@ -39,94 +39,59 @@ export default {
type: String,
required: true,
},
- store: {
- type: Object,
- required: true,
- },
- service: {
- type: Object,
- required: true,
- },
},
data() {
return {
- state: this.store.state,
- isLoading: false,
- isLoadingStage: false,
- isEmptyStage: false,
- hasError: true,
- startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
};
},
computed: {
- currentStage() {
- return this.store.currentActiveStage();
+ ...mapState([
+ 'isLoading',
+ 'isLoadingStage',
+ 'isEmptyStage',
+ 'selectedStage',
+ 'selectedStageEvents',
+ 'stages',
+ 'summary',
+ 'startDate',
+ ]),
+ displayStageEvents() {
+ const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
+ return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
+ },
+ displayNotEnoughData() {
+ const { selectedStage, isEmptyStage, isLoadingStage } = this;
+ return selectedStage && isEmptyStage && !isLoadingStage;
+ },
+ displayNoAccess() {
+ const { selectedStage } = this;
+ return selectedStage && !selectedStage.isUserAllowed;
},
- },
- created() {
- this.fetchCycleAnalyticsData();
},
methods: {
- handleError() {
- this.store.setErrorState(true);
- return new Flash(__('There was an error while fetching value stream analytics data.'));
- },
+ ...mapActions([
+ 'fetchCycleAnalyticsData',
+ 'fetchStageData',
+ 'setSelectedStage',
+ 'setDateRange',
+ ]),
handleDateSelect(startDate) {
- this.startDate = startDate;
- this.fetchCycleAnalyticsData({ startDate: this.startDate });
+ this.setDateRange({ startDate });
+ this.fetchCycleAnalyticsData();
},
- fetchCycleAnalyticsData(options) {
- const fetchOptions = options || { startDate: this.startDate };
-
- this.isLoading = true;
-
- this.service
- .fetchCycleAnalyticsData(fetchOptions)
- .then((response) => {
- this.store.setCycleAnalyticsData(response);
- this.selectDefaultStage();
- })
- .catch(() => {
- this.handleError();
- })
- .finally(() => {
- this.isLoading = false;
- });
- },
- selectDefaultStage() {
- const stage = this.state.stages[0];
- this.selectStage(stage);
+ isActiveStage(stage) {
+ return stage.slug === this.selectedStage.slug;
},
selectStage(stage) {
- if (this.isLoadingStage) return;
- if (this.currentStage === stage) return;
+ if (this.selectedStage === stage) return;
+ this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
- this.store.setActiveStage(stage);
return;
}
- this.isLoadingStage = true;
- this.store.setStageEvents([], stage);
- this.store.setActiveStage(stage);
-
- this.service
- .fetchStageData({
- stage,
- startDate: this.startDate,
- projectIds: this.selectedProjectIds,
- })
- .then((response) => {
- this.isEmptyStage = !response.events.length;
- this.store.setStageEvents(response.events, stage);
- })
- .catch(() => {
- this.isEmptyStage = true;
- })
- .finally(() => {
- this.isLoadingStage = false;
- });
+ this.fetchStageData();
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
@@ -146,12 +111,13 @@ export default {
<div class="card">
<div class="card-header">{{ __('Recent Project Activity') }}</div>
<div class="d-flex justify-content-between">
- <div v-for="item in state.summary" :key="item.title" class="flex-grow text-center">
+ <div v-for="item in summary" :key="item.title" class="gl-flex-grow-1 gl-text-center">
<h3 class="header">{{ item.value }}</h3>
<p class="text">{{ item.title }}</p>
</div>
<div class="flex-grow align-self-center text-center">
<div class="js-ca-dropdown dropdown inline">
+ <!-- eslint-disable-next-line @gitlab/vue-no-data-toggle -->
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
<span class="dropdown-label">
<gl-sprintf :message="$options.i18n.dropdownText">
@@ -207,11 +173,9 @@ export default {
</span>
</li>
<li class="event-header pl-3">
- <span
- v-if="currentStage && currentStage.legend"
- class="stage-name font-weight-bold"
- >{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}</span
- >
+ <span v-if="selectedStage" class="stage-name font-weight-bold">{{
+ selectedStage.legend ? __(selectedStage.legend) : __('Related Issues')
+ }}</span>
<span
class="has-tooltip"
data-placement="top"
@@ -242,19 +206,19 @@ export default {
<nav class="stage-nav">
<ul>
<stage-nav-item
- v-for="stage in state.stages"
+ v-for="stage in stages"
:key="stage.title"
:title="stage.title"
:is-user-allowed="stage.isUserAllowed"
:value="stage.value"
- :is-active="stage.active"
+ :is-active="isActiveStage(stage)"
@select="selectStage(stage)"
/>
</ul>
</nav>
<section class="stage-events overflow-auto">
<gl-loading-icon v-show="isLoadingStage" size="lg" />
- <template v-if="currentStage && !currentStage.isUserAllowed">
+ <template v-if="displayNoAccess">
<gl-empty-state
class="js-empty-state"
:title="__('You need permission.')"
@@ -263,19 +227,19 @@ export default {
/>
</template>
<template v-else>
- <template v-if="currentStage && isEmptyStage && !isLoadingStage">
+ <template v-if="displayNotEnoughData">
<gl-empty-state
class="js-empty-state"
- :description="currentStage.emptyStageText"
+ :description="selectedStage.emptyStageText"
:svg-path="noDataSvgPath"
:title="__('We don\'t have enough data to show this stage.')"
/>
</template>
- <template v-if="state.events.length && !isLoadingStage && !isEmptyStage">
+ <template v-if="displayStageEvents">
<component
- :is="currentStage.component"
- :stage="currentStage"
- :items="state.events"
+ :is="selectedStage.component"
+ :stage="selectedStage"
+ :items="selectedStageEvents"
/>
</template>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
new file mode 100644
index 00000000000..d79de207afe
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -0,0 +1 @@
+export const DEFAULT_DAYS_TO_DISPLAY = 30;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
deleted file mode 100644
index d7fcda24352..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-export default class CycleAnalyticsService {
- constructor(options) {
- this.axios = axios.create({
- baseURL: options.requestPath,
- });
- }
-
- fetchCycleAnalyticsData(options = { startDate: 30 }) {
- const { startDate, projectIds } = options;
-
- return this.axios
- .get('', {
- params: {
- 'cycle_analytics[start_date]': startDate,
- 'cycle_analytics[project_ids]': projectIds,
- },
- })
- .then((x) => x.data);
- }
-
- fetchStageData(options) {
- const { stage, startDate, projectIds } = options;
-
- return this.axios
- .get(`events/${stage.name}.json`, {
- params: {
- 'cycle_analytics[start_date]': startDate,
- 'cycle_analytics[project_ids]': projectIds,
- },
- })
- .then((x) => x.data);
- }
-}
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
deleted file mode 100644
index 24ad6ef4c88..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import { dasherize } from '../lib/utils/text_utility';
-import { __ } from '../locale';
-import DEFAULT_EVENT_OBJECTS from './default_event_objects';
-
-const EMPTY_STAGE_TEXTS = {
- issue: __(
- 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
- ),
- plan: __(
- 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
- ),
- code: __(
- 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
- ),
- test: __(
- 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
- ),
- review: __(
- 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
- ),
- staging: __(
- 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
- ),
-};
-
-export default {
- state: {
- summary: '',
- stats: '',
- analytics: '',
- events: [],
- stages: [],
- },
- setCycleAnalyticsData(data) {
- this.state = Object.assign(this.state, this.decorateData(data));
- },
- decorateData(data) {
- const newData = {};
-
- newData.stages = data.stats || [];
- newData.summary = data.summary || [];
-
- newData.summary.forEach((item) => {
- item.value = item.value || '-';
- });
-
- newData.stages.forEach((item) => {
- const stageSlug = dasherize(item.name.toLowerCase());
- item.active = false;
- item.isUserAllowed = data.permissions[stageSlug];
- item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
- item.component = `stage-${stageSlug}-component`;
- item.slug = stageSlug;
- });
- newData.analytics = data;
- return newData;
- },
- setLoadingState(state) {
- this.state.isLoading = state;
- },
- setErrorState(state) {
- this.state.hasError = state;
- },
- deactivateAllStages() {
- this.state.stages.forEach((stage) => {
- stage.active = false;
- });
- },
- setActiveStage(stage) {
- this.deactivateAllStages();
- stage.active = true;
- },
- setStageEvents(events, stage) {
- this.state.events = this.decorateEvents(events, stage);
- },
- decorateEvents(events, stage) {
- const newEvents = [];
-
- events.forEach((item) => {
- if (!item) return;
-
- const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item };
-
- eventItem.totalTime = eventItem.total_time;
-
- if (eventItem.author) {
- eventItem.author.webUrl = eventItem.author.web_url;
- eventItem.author.avatarUrl = eventItem.author.avatar_url;
- }
-
- if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
- if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
- if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
-
- delete eventItem.author.web_url;
- delete eventItem.author.avatar_url;
- delete eventItem.total_time;
- delete eventItem.created_at;
- delete eventItem.short_sha;
- delete eventItem.commit_url;
-
- newEvents.push(eventItem);
- });
-
- return newEvents;
- },
- currentActiveStage() {
- return this.state.stages.find((stage) => stage.active);
- },
-};
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 42d6700fae1..00192cc61f8 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -1,31 +1,29 @@
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
-import CycleAnalyticsService from './cycle_analytics_service';
-import CycleAnalyticsStore from './cycle_analytics_store';
+import createStore from './store';
Vue.use(Translate);
-const createCycleAnalyticsService = (requestPath) =>
- new CycleAnalyticsService({
- requestPath,
- });
-
export default () => {
+ const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const { noAccessSvgPath, noDataSvgPath } = el.dataset;
+ const { noAccessSvgPath, noDataSvgPath, requestPath } = el.dataset;
+
+ store.dispatch('initializeVsa', {
+ requestPath,
+ });
// eslint-disable-next-line no-new
new Vue({
el,
name: 'CycleAnalytics',
+ store,
render: (createElement) =>
createElement(CycleAnalytics, {
props: {
noDataSvgPath,
noAccessSvgPath,
- store: CycleAnalyticsStore,
- service: createCycleAnalyticsService(el.dataset.requestPath),
},
}),
});
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
new file mode 100644
index 00000000000..fe3c6d6b3ba
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -0,0 +1,51 @@
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
+import * as types from './mutation_types';
+
+export const fetchCycleAnalyticsData = ({
+ state: { requestPath, startDate },
+ dispatch,
+ commit,
+}) => {
+ commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
+
+ return axios
+ .get(requestPath, {
+ params: { 'cycle_analytics[start_date]': startDate },
+ })
+ .then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
+ .then(() => dispatch('setSelectedStage'))
+ .then(() => dispatch('fetchStageData'))
+ .catch(() => {
+ commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
+ createFlash({
+ message: __('There was an error while fetching value stream analytics data.'),
+ });
+ });
+};
+
+export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
+ commit(types.REQUEST_STAGE_DATA);
+
+ return axios
+ .get(`${requestPath}/events/${selectedStage.name}.json`, {
+ params: { 'cycle_analytics[start_date]': startDate },
+ })
+ .then(({ data }) => commit(types.RECEIVE_STAGE_DATA_SUCCESS, data))
+ .catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
+};
+
+export const setSelectedStage = ({ commit, state: { stages } }, selectedStage = null) => {
+ const stage = selectedStage || stages[0];
+ commit(types.SET_SELECTED_STAGE, stage);
+};
+
+export const setDateRange = ({ commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) =>
+ commit(types.SET_DATE_RANGE, { startDate });
+
+export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
+ commit(types.INITIALIZE_VSA, initialData);
+ return dispatch('fetchCycleAnalyticsData');
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/cycle_analytics/store/index.js
new file mode 100644
index 00000000000..ab47538dcf5
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/index.js
@@ -0,0 +1,21 @@
+/**
+ * While we are in the process implementing group level features at the project level
+ * we will use a simplified vuex store for the project level, eventually this can be
+ * replaced with the store at ee/app/assets/javascripts/analytics/cycle_analytics/store/index.js
+ * once we have enough of the same features implemented across the project and group level
+ */
+
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state,
+ });
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
new file mode 100644
index 00000000000..00aae49ae9f
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
@@ -0,0 +1,12 @@
+export const INITIALIZE_VSA = 'INITIALIZE_VSA';
+
+export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
+export const SET_DATE_RANGE = 'SET_DATE_RANGE';
+
+export const REQUEST_CYCLE_ANALYTICS_DATA = 'REQUEST_CYCLE_ANALYTICS_DATA';
+export const RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS = 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS';
+export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_ERROR';
+
+export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
+export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
+export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
new file mode 100644
index 00000000000..8fd5c78339a
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -0,0 +1,52 @@
+import { decorateData, decorateEvents } from '../utils';
+import * as types from './mutation_types';
+
+export default {
+ [types.INITIALIZE_VSA](state, { requestPath }) {
+ state.requestPath = requestPath;
+ },
+ [types.SET_SELECTED_STAGE](state, stage) {
+ state.isLoadingStage = true;
+ state.selectedStage = stage;
+ state.isLoadingStage = false;
+ },
+ [types.SET_DATE_RANGE](state, { startDate }) {
+ state.startDate = startDate;
+ },
+ [types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
+ state.isLoading = true;
+ state.stages = [];
+ state.hasError = false;
+ },
+ [types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
+ state.isLoading = false;
+ const { stages, summary } = decorateData(data);
+ state.stages = stages;
+ state.summary = summary;
+ state.hasError = false;
+ },
+ [types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
+ state.isLoading = false;
+ state.stages = [];
+ state.hasError = true;
+ },
+ [types.REQUEST_STAGE_DATA](state) {
+ state.isLoadingStage = true;
+ state.isEmptyStage = false;
+ state.selectedStageEvents = [];
+ state.hasError = false;
+ },
+ [types.RECEIVE_STAGE_DATA_SUCCESS](state, { events = [] }) {
+ const { selectedStage } = state;
+ state.isLoadingStage = false;
+ state.isEmptyStage = !events.length;
+ state.selectedStageEvents = decorateEvents(events, selectedStage);
+ state.hasError = false;
+ },
+ [types.RECEIVE_STAGE_DATA_ERROR](state) {
+ state.isLoadingStage = false;
+ state.isEmptyStage = true;
+ state.selectedStageEvents = [];
+ state.hasError = true;
+ },
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
new file mode 100644
index 00000000000..5db4e1878a9
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -0,0 +1,17 @@
+import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
+
+export default () => ({
+ requestPath: '',
+ startDate: DEFAULT_DAYS_TO_DISPLAY,
+ stages: [],
+ summary: [],
+ analytics: [],
+ stats: [],
+ selectedStage: {},
+ selectedStageEvents: [],
+ medians: {},
+ hasError: false,
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
new file mode 100644
index 00000000000..3afe4b021be
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -0,0 +1,63 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { dasherize } from '~/lib/utils/text_utility';
+import { __ } from '../locale';
+import DEFAULT_EVENT_OBJECTS from './default_event_objects';
+
+const EMPTY_STAGE_TEXTS = {
+ issue: __(
+ 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
+ ),
+ plan: __(
+ 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
+ ),
+ code: __(
+ 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
+ ),
+ test: __(
+ 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
+ ),
+ review: __(
+ 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
+ ),
+ staging: __(
+ 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
+ ),
+};
+
+/**
+ * These `decorate` methods will be removed when me migrate to the
+ * new table layout https://gitlab.com/gitlab-org/gitlab/-/issues/326704
+ */
+const mapToEvent = (event, stage) => {
+ return convertObjectPropsToCamelCase(
+ {
+ ...DEFAULT_EVENT_OBJECTS[stage.slug],
+ ...event,
+ },
+ { deep: true },
+ );
+};
+
+export const decorateEvents = (events, stage) => events.map((event) => mapToEvent(event, stage));
+
+const mapToStage = (permissions, item) => {
+ const slug = dasherize(item.name.toLowerCase());
+ return {
+ ...item,
+ slug,
+ active: false,
+ isUserAllowed: permissions[slug],
+ emptyStageText: EMPTY_STAGE_TEXTS[slug],
+ component: `stage-${slug}-component`,
+ };
+};
+
+const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
+
+export const decorateData = (data = {}) => {
+ const { permissions, stats, summary } = data;
+ return {
+ stages: stats?.map((item) => mapToStage(permissions, item)) || [],
+ summary: summary?.map((item) => mapToSummary(item)) || [],
+ };
+};
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
index e62000c007c..fdd1ea6e32e 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutations.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -7,7 +7,7 @@ const formatTimezoneName = (freezePeriod, timezoneList) =>
cron_timezone: {
formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)
?.name,
- identifier: freezePeriod.cronTimezone,
+ identifier: freezePeriod.cron_timezone,
},
});
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index af7c391ab70..7bc1eb5d652 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,10 +1,10 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import eventHub from '../eventhub';
export default {
components: {
- GlLoadingIcon,
+ GlButton,
},
props: {
deployKey: {
@@ -15,10 +15,20 @@ export default {
type: String,
required: true,
},
- btnCssClass: {
+ category: {
type: String,
required: false,
- default: 'btn-default',
+ default: 'tertiary',
+ },
+ variant: {
+ type: String,
+ required: false,
+ default: 'default',
+ },
+ icon: {
+ type: String,
+ required: false,
+ default: '',
},
},
data() {
@@ -39,13 +49,14 @@ export default {
</script>
<template>
- <button
- :class="[{ disabled: isLoading }, btnCssClass]"
- :disabled="isLoading"
+ <gl-button
+ :category="category"
+ :variant="variant"
+ :icon="icon"
+ :loading="isLoading"
class="btn"
@click="doAction"
>
<slot></slot>
- <gl-loading-icon v-if="isLoading" :inline="true" />
- </button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 425cca13ae8..02c57164f47 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -6,10 +6,12 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
+import ConfirmModal from './confirm_modal.vue';
import KeysPanel from './keys_panel.vue';
export default {
components: {
+ ConfirmModal,
KeysPanel,
NavigationTabs,
GlLoadingIcon,
@@ -30,6 +32,9 @@ export default {
currentTab: 'enabled_keys',
isLoading: false,
store: new DeployKeysStore(),
+ removeKey: () => {},
+ cancel: () => {},
+ confirmModalVisible: false,
};
},
scopes: {
@@ -61,16 +66,16 @@ export default {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.disableKey);
- eventHub.$on('disable.key', this.disableKey);
+ eventHub.$on('remove.key', this.confirmRemoveKey);
+ eventHub.$on('disable.key', this.confirmRemoveKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.disableKey);
- eventHub.$off('disable.key', this.disableKey);
+ eventHub.$off('remove.key', this.confirmRemoveKey);
+ eventHub.$off('disable.key', this.confirmRemoveKey);
},
methods: {
onChangeTab(tab) {
@@ -97,19 +102,20 @@ export default {
.then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
- disableKey(deployKey, callback) {
- if (
- // eslint-disable-next-line no-alert
- window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))
- ) {
+ confirmRemoveKey(deployKey, callback) {
+ const hideModal = () => {
+ this.confirmModalVisible = false;
+ callback?.();
+ };
+ this.removeKey = () => {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
- .then(callback)
+ .then(hideModal)
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
- } else {
- callback();
- }
+ };
+ this.cancel = hideModal;
+ this.confirmModalVisible = true;
},
},
};
@@ -117,6 +123,7 @@ export default {
<template>
<div class="gl-mb-3 deploy-keys">
+ <confirm-modal :visible="confirmModalVisible" @remove="removeKey" @cancel="cancel" />
<gl-loading-icon
v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')"
@@ -124,8 +131,12 @@ export default {
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
- <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
+ <div class="fade-left">
+ <gl-icon name="chevron-lg-left" :size="12" />
+ </div>
+ <div class="fade-right">
+ <gl-icon name="chevron-lg-right" :size="12" />
+ </div>
<navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" />
</div>
@@ -134,7 +145,7 @@ export default {
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
- data-qa-selector="project_deploy_keys"
+ data-qa-selector="project_deploy_keys_container"
/>
</template>
</div>
diff --git a/app/assets/javascripts/deploy_keys/components/confirm_modal.vue b/app/assets/javascripts/deploy_keys/components/confirm_modal.vue
new file mode 100644
index 00000000000..1932435c42a
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/components/confirm_modal.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ body: __(
+ 'Are you sure you want to remove this deploy key? If anything is still using this key, it will stop working.',
+ ),
+ },
+ modalOptions: {
+ title: __('Do you want to remove this deploy key?'),
+ actionPrimary: {
+ text: __('Remove deploy key'),
+ attributes: [{ variant: 'danger' }],
+ },
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: [{ category: 'tertiary' }],
+ },
+ static: true,
+ modalId: 'confirm-remove-deploy-key',
+ },
+};
+</script>
+<template>
+ <gl-modal
+ v-bind="$options.modalOptions"
+ :visible="visible"
+ @primary="$emit('remove')"
+ @secondary="$emit('cancel')"
+ @hidden="$emit('cancel')"
+ >
+ {{ $options.i18n.body }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index e70ca18bb71..8a7d3430063 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlLink, GlTooltipDirective, GlButton } from '@gitlab/ui';
import { head, tail } from 'lodash';
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -9,7 +9,9 @@ import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
+ GlButton,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -111,9 +113,9 @@ export default {
<div class="gl-responsive-table-row deploy-key">
<div class="table-section section-40">
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div>
- <div class="table-mobile-content qa-key">
- <strong class="title qa-key-title"> {{ deployKey.title }} </strong>
- <div class="fingerprint" data-qa-selector="key_md5_fingerprint">
+ <div class="table-mobile-content" data-qa-selector="key_container">
+ <strong class="title" data-qa-selector="key_title_content"> {{ deployKey.title }} </strong>
+ <div class="fingerprint" data-qa-selector="key_md5_fingerprint_content">
{{ __('MD5') }}:{{ deployKey.fingerprint }}
</div>
<div class="fingerprint">{{ __('SHA256') }}:{{ deployKey.fingerprint_sha256 }}</div>
@@ -123,15 +125,15 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Project usage') }}</div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
- <a
+ <gl-link
v-gl-tooltip
:title="projectTooltipTitle(firstProject)"
class="label deploy-project-label"
>
<span> {{ firstProject.project.full_name }} </span>
<gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
- </a>
- <a
+ </gl-link>
+ <gl-link
v-if="isExpandable"
v-gl-tooltip
:title="restProjectsTooltip"
@@ -139,8 +141,8 @@ export default {
@click="toggleExpanded"
>
<span>{{ restProjectsLabel }}</span>
- </a>
- <a
+ </gl-link>
+ <gl-link
v-for="deployKeysProject in restProjects"
v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
@@ -151,7 +153,7 @@ export default {
>
<span> {{ deployKeysProject.project.full_name }} </span>
<gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
- </a>
+ </gl-link>
</template>
<span v-else class="text-secondary">{{ __('None') }}</span>
</div>
@@ -166,41 +168,43 @@ export default {
</div>
<div class="table-section section-15 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
- <action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable">
+ <action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable" category="secondary">
{{ __('Enable') }}
</action-btn>
- <a
+ <gl-button
v-if="deployKey.can_edit"
v-gl-tooltip
:href="editDeployKeyPath"
:title="__('Edit')"
- class="btn btn-default text-secondary"
+ :aria-label="__('Edit')"
data-container="body"
- >
- <gl-icon name="pencil" />
- </a>
+ icon="pencil"
+ category="secondary"
+ />
<action-btn
v-if="isRemovable"
v-gl-tooltip
:deploy-key="deployKey"
:title="__('Remove')"
- btn-css-class="btn-danger"
+ :aria-label="__('Remove')"
+ category="primary"
+ variant="danger"
+ icon="remove"
type="remove"
data-container="body"
- >
- <gl-icon name="remove" />
- </action-btn>
+ />
<action-btn
v-else-if="isEnabled"
v-gl-tooltip
:deploy-key="deployKey"
:title="__('Disable')"
- btn-css-class="btn-warning"
+ :aria-label="__('Disable')"
type="disable"
data-container="body"
- >
- <gl-icon name="cancel" />
- </action-btn>
+ icon="cancel"
+ category="primary"
+ variant="danger"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index fb25d3618ab..336ce714a05 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -115,12 +115,13 @@ export default {
</template>
</markdown-field>
<slot name="resolve-checkbox"></slot>
- <div class="note-form-actions gl-display-flex gl-justify-content-space-between">
+ <div class="note-form-actions gl-display-flex">
<gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ class="gl-mr-3 gl-w-auto!"
category="primary"
- variant="success"
+ variant="confirm"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
@@ -128,9 +129,14 @@ export default {
>
{{ buttonText }}
</gl-button>
- <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button
+ ref="cancelButton"
+ class="gl-w-auto!"
+ variant="default"
+ category="primary"
+ @click="cancelComment"
+ >{{ __('Cancel') }}</gl-button
+ >
</div>
<gl-modal
ref="cancelCommentModal"
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 04d80dc0069..ad557f64ce4 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -333,7 +333,7 @@ export default {
ghostClass: 'gl-visibility-hidden',
},
i18n: {
- dropzoneDescriptionText: __('Drop or %{linkStart}upload%{linkEnd} designs to attach'),
+ dropzoneDescriptionText: __('Drag your designs here or %{linkStart}click to upload%{linkEnd}.'),
},
};
</script>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 7c610968209..6a3f5993a22 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -3,6 +3,8 @@ import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Mousetrap from 'mousetrap';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import api from '~/api';
import {
keysFor,
MR_PREVIOUS_FILE_IN_DIFF,
@@ -16,7 +18,6 @@ import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '../../notes/event_hub';
import {
@@ -30,6 +31,15 @@ import {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES,
+ INLINE_DIFF_VIEW_TYPE,
+ TRACKING_DIFF_VIEW_INLINE,
+ TRACKING_DIFF_VIEW_PARALLEL,
+ TRACKING_FILE_BROWSER_TREE,
+ TRACKING_FILE_BROWSER_LIST,
+ TRACKING_WHITESPACE_SHOW,
+ TRACKING_WHITESPACE_HIDE,
+ TRACKING_SINGLE_FILE_MODE,
+ TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
import { reviewStatuses } from '../utils/file_reviews';
@@ -59,8 +69,9 @@ export default {
PanelResizer,
GlPagination,
GlSprintf,
+ DynamicScroller,
+ DynamicScrollerItem,
},
- mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
@@ -183,8 +194,15 @@ export default {
'hasConflicts',
'viewDiffsFileByFile',
'mrReviews',
+ 'renderTreeList',
+ 'showWhitespace',
+ ]),
+ ...mapGetters('diffs', [
+ 'whichCollapsedTypes',
+ 'isParallelView',
+ 'currentDiffIndex',
+ 'isVirtualScrollingEnabled',
]),
- ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']),
...mapGetters('batchComments', ['draftsCount']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
@@ -305,6 +323,32 @@ export default {
if (id && id.indexOf('#note') !== 0) {
this.setHighlightedRow(id.split('diff-content').pop().slice(1));
}
+
+ if (window.gon?.features?.diffSettingsUsageData) {
+ if (this.renderTreeList) {
+ api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_LIST);
+ }
+
+ if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
+ api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_INLINE);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_PARALLEL);
+ }
+
+ if (this.showWhitespace) {
+ api.trackRedisHllUserEvent(TRACKING_WHITESPACE_SHOW);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_WHITESPACE_HIDE);
+ }
+
+ if (this.viewDiffsFileByFile) {
+ api.trackRedisHllUserEvent(TRACKING_SINGLE_FILE_MODE);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_MULTIPLE_FILES_MODE);
+ }
+ }
},
beforeCreate() {
diffsApp.instrument();
@@ -523,17 +567,41 @@ export default {
<commit-widget v-if="commit" :commit="commit" :collapsible="false" />
<div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
<template v-else-if="renderDiffFiles">
- <diff-file
- v-for="(file, index) in diffs"
- :key="file.newPath"
- :file="file"
- :reviewed="fileReviews[file.id]"
- :is-first-file="index === 0"
- :is-last-file="index === diffFilesLength - 1"
- :help-page-path="helpPagePath"
- :can-current-user-fork="canCurrentUserFork"
- :view-diffs-file-by-file="viewDiffsFileByFile"
- />
+ <dynamic-scroller
+ v-if="isVirtualScrollingEnabled"
+ :items="diffs"
+ :min-item-size="70"
+ :buffer="1000"
+ :use-transform="false"
+ page-mode
+ >
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <diff-file
+ :file="item"
+ :reviewed="fileReviews[item.id]"
+ :is-first-file="index === 0"
+ :is-last-file="index === diffFilesLength - 1"
+ :help-page-path="helpPagePath"
+ :can-current-user-fork="canCurrentUserFork"
+ :view-diffs-file-by-file="viewDiffsFileByFile"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ <template v-else>
+ <diff-file
+ v-for="(file, index) in diffs"
+ :key="file.new_path"
+ :file="file"
+ :reviewed="fileReviews[file.id]"
+ :is-first-file="index === 0"
+ :is-last-file="index === diffFilesLength - 1"
+ :help-page-path="helpPagePath"
+ :can-current-user-fork="canCurrentUserFork"
+ :view-diffs-file-by-file="viewDiffsFileByFile"
+ />
+ </template>
<div
v-if="showFileByFileNavigation"
data-testid="file-by-file-navigation"
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index bc0f2fb0b69..820c64a9502 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -138,7 +138,7 @@ export default {
/>
</div>
<div class="commit-detail flex-list">
- <div class="commit-content qa-commit-content">
+ <div class="commit-content" data-qa-selector="commit_content">
<a
:href="commit.commit_url"
class="commit-row-message item-title"
@@ -173,7 +173,7 @@ export default {
<pre
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
- class="commit-row-description gl-mb-3 text-dark"
+ class="commit-row-description gl-mb-3 gl-text-body"
v-html="commitDescription"
></pre>
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
index 2c249f71091..6c5973b7c28 100644
--- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
+++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue
@@ -1,11 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
+ GlDropdownDivider,
TimeAgo,
},
props: {
@@ -24,34 +25,38 @@ export default {
<template>
<gl-dropdown :text="selectedVersionName" data-qa-selector="dropdown_content">
- <gl-dropdown-item
- v-for="version in versions"
- :key="version.id"
- :class="{
- 'is-active': version.selected,
- }"
- :is-check-item="true"
- :is-checked="version.selected"
- :href="version.href"
- >
- <div>
- <strong>
- {{ version.versionName }}
- <template v-if="version.isHead">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
- <template v-else-if="version.isBase">{{ s__('DiffsCompareBaseBranch|(base)') }}</template>
- </strong>
- </div>
- <div>
- <small class="commit-sha"> {{ version.short_commit_sha }} </small>
- </div>
- <div>
- <small>
- <template v-if="version.commitsText">
- {{ version.commitsText }}
- </template>
- <time-ago v-if="version.created_at" :time="version.created_at" class="js-timeago" />
- </small>
- </div>
- </gl-dropdown-item>
+ <template v-for="version in versions">
+ <gl-dropdown-divider v-if="version.addDivider" :key="version.id" />
+ <gl-dropdown-item
+ :key="version.id"
+ :class="{
+ 'is-active': version.selected,
+ }"
+ :is-check-item="true"
+ :is-checked="version.selected"
+ :href="version.href"
+ >
+ <div>
+ <strong>
+ {{ version.versionName }}
+ <template v-if="version.isHead">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
+ <template v-else-if="version.isBase">{{
+ s__('DiffsCompareBaseBranch|(base)')
+ }}</template>
+ </strong>
+ </div>
+ <div>
+ <small class="commit-sha"> {{ version.short_commit_sha }} </small>
+ </div>
+ <div>
+ <small>
+ <template v-if="version.commitsText">
+ {{ version.commitsText }}
+ </template>
+ <time-ago v-if="version.created_at" :time="version.created_at" class="js-timeago" />
+ </small>
+ </div>
+ </gl-dropdown-item>
+ </template>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 7526c5347f7..e2a1f7236c5 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -142,7 +142,7 @@ export default {
</gl-button-group>
</div>
<gl-sprintf
- v-else-if="hasSourceVersions"
+ v-else-if="!commit && hasSourceVersions"
class="d-flex align-items-center compare-versions-container"
:message="s__('MergeRequest|Compare %{target} and %{source}')"
>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 663d2bb3cf8..283dbc6031c 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -49,9 +49,7 @@ export default {
},
},
computed: {
- ...mapState({
- projectPath: (state) => state.diffs.projectPath,
- }),
+ ...mapState('diffs', ['projectPath']),
...mapGetters('diffs', [
'isInlineView',
'isParallelView',
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index bdbc13a38c4..ce867dbb9e0 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -5,6 +5,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
+import { scrollToElement } from '~/lib/utils/common_utils';
import { sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '../../notes/event_hub';
@@ -82,7 +83,7 @@ export default {
computed: {
...mapState('diffs', ['currentDiffFileId', 'codequalityDiff']),
...mapGetters(['isNotesFetched']),
- ...mapGetters('diffs', ['getDiffFileDiscussions']),
+ ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled']),
viewBlobHref() {
return escape(this.file.view_path);
},
@@ -148,10 +149,8 @@ export default {
return loggedIn && featureOn;
},
- hasCodequalityChanges() {
- return (
- this.codequalityDiff?.files && this.codequalityDiff?.files[this.file.file_path]?.length > 0
- );
+ codequalityDiffForFile() {
+ return this.codequalityDiff?.files?.[this.file.file_path] || [];
},
},
watch: {
@@ -235,15 +234,20 @@ export default {
eventHub.$emit(event);
});
},
- handleToggle() {
- const currentCollapsedFlag = this.isCollapsed;
+ handleToggle({ viaUserInteraction = false } = {}) {
+ const collapsingNow = !this.isCollapsed;
+ const contentElement = this.$el.querySelector(`#diff-content-${this.file.file_hash}`);
this.setFileCollapsedByUser({
filePath: this.file.file_path,
- collapsed: !currentCollapsedFlag,
+ collapsed: collapsingNow,
});
- if (!this.hasDiff && currentCollapsedFlag) {
+ if (collapsingNow && viaUserInteraction && contentElement) {
+ scrollToElement(contentElement, { duration: 1 });
+ }
+
+ if (!this.hasDiff && !collapsingNow) {
this.requestDiff();
}
},
@@ -286,6 +290,7 @@ export default {
'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink),
'has-body': showBody,
+ 'is-virtual-scrolling': isVirtualScrollingEnabled,
}"
:data-path="file.new_path"
class="diff-file file-holder gl-border-none"
@@ -299,10 +304,10 @@ export default {
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
:show-local-file-reviews="showLocalFileReviews"
- :has-codequality-changes="hasCodequalityChanges"
+ :codequality-diff="codequalityDiffForFile"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
- @toggleFile="handleToggle"
+ @toggleFile="handleToggle({ viaUserInteraction: true })"
@showForkMessage="showForkMessage"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 3b4e21ab61b..676c9a3c7bc 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -96,10 +96,10 @@ export default {
required: false,
default: false,
},
- hasCodequalityChanges: {
- type: Boolean,
+ codequalityDiff: {
+ type: Array,
required: false,
- default: false,
+ default: () => [],
},
},
data() {
@@ -333,7 +333,12 @@ export default {
data-track-property="diff_copy_file"
/>
- <code-quality-badge v-if="hasCodequalityChanges" class="gl-mr-2" />
+ <code-quality-badge
+ v-if="codequalityDiff.length"
+ :file-name="filePath"
+ :codequality-diff="codequalityDiff"
+ class="gl-mr-2"
+ />
<small v-if="isModeChanged" ref="fileMode" class="mr-1">
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
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 51da1966630..c907b5dffaf 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -207,14 +207,14 @@ export default {
</div>
<note-form
ref="noteForm"
- :is-editing="true"
+ :is-editing="false"
:line-code="line.line_code"
:line="line"
:lines="commentLines"
:help-page-path="helpPagePath"
:diff-file="diffFile"
:show-suggest-popover="showSuggestPopover"
- save-button-title="Comment"
+ :save-button-title="__('Comment')"
class="diff-comment-form gl-mt-3"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 8d398a2ded4..d4a1a9e0e46 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -204,27 +204,33 @@ export default {
<template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
<div
:class="classNameMapCellLeft"
- data-testid="leftLineNumber"
+ data-testid="left-line-number"
class="diff-td diff-line-num"
+ data-qa-selector="new_diff_line_link"
>
<template v-if="!isLeftConflictMarker">
<span
v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft"
v-gl-tooltip
- data-testid="leftCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipLeft"
>
- <button
- :draggable="glFeatures.dragCommentSelection"
+ <div
+ data-testid="left-comment-button"
+ role="button"
+ tabindex="0"
+ :draggable="!line.left.commentsDisabled && glFeatures.dragCommentSelection"
type="button"
- class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
data-qa-selector="diff_comment_button"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled"
- @click="handleCommentButton(line.left)"
- @dragstart="onDragStart({ ...line.left, index })"
- ></button>
+ :aria-disabled="line.left.commentsDisabled"
+ @click="!line.left.commentsDisabled && handleCommentButton(line.left)"
+ @keydown.enter="!line.left.commentsDisabled && handleCommentButton(line.left)"
+ @keydown.space="!line.left.commentsDisabled && handleCommentButton(line.left)"
+ @dragstart="!line.left.commentsDisabled && onDragStart({ ...line.left, index })"
+ ></div>
</span>
</template>
<a
@@ -238,7 +244,7 @@ export default {
v-if="line.hasDiscussionsLeft"
:discussions="line.left.discussions"
:discussions-expanded="line.left.discussionsExpanded"
- data-testid="leftDiscussions"
+ data-testid="left-discussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.left.line_code,
@@ -268,7 +274,7 @@ export default {
:key="line.left.line_code"
:class="[parallelViewLeftLineType, { parallel: !inline }]"
class="diff-td line_content with-coverage left-side"
- data-testid="leftContent"
+ data-testid="left-content"
@mousedown="handleParallelLineMouseDown"
>
<strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong>
@@ -277,7 +283,7 @@ export default {
</template>
<template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)">
<div
- data-testid="leftEmptyCell"
+ data-testid="left-empty-cell"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellLeftClassMap"
>
@@ -313,19 +319,24 @@ export default {
<span
v-if="shouldRenderCommentButton && !line.hasDiscussionsRight"
v-gl-tooltip
- data-testid="rightCommentButton"
class="add-diff-note tooltip-wrapper"
:title="addCommentTooltipRight"
>
- <button
- :draggable="glFeatures.dragCommentSelection"
+ <div
+ data-testid="right-comment-button"
+ role="button"
+ tabindex="0"
+ :draggable="!line.right.commentsDisabled && glFeatures.dragCommentSelection"
type="button"
- class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled"
- @click="handleCommentButton(line.right)"
- @dragstart="onDragStart({ ...line.right, index })"
- ></button>
+ :aria-disabled="line.right.commentsDisabled"
+ @click="!line.right.commentsDisabled && handleCommentButton(line.right)"
+ @keydown.enter="!line.right.commentsDisabled && handleCommentButton(line.right)"
+ @keydown.space="!line.right.commentsDisabled && handleCommentButton(line.right)"
+ @dragstart="!line.right.commentsDisabled && onDragStart({ ...line.right, index })"
+ ></div>
</span>
</template>
<a
@@ -339,7 +350,7 @@ export default {
v-if="line.hasDiscussionsRight"
:discussions="line.right.discussions"
:discussions-expanded="line.right.discussionsExpanded"
- data-testid="rightDiscussions"
+ data-testid="right-discussions"
@toggleLineDiscussions="
toggleLineDiscussions({
lineCode: line.right.line_code,
@@ -381,7 +392,7 @@ export default {
</template>
<template v-else>
<div
- data-testid="rightEmptyCell"
+ data-testid="right-empty-cell"
class="diff-td diff-line-num old_line empty-cell"
:class="emptyCellRightClassMap"
></div>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 25403b1547e..f903fef72b7 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -177,7 +177,6 @@ export default {
<a
v-if="line.new_line"
ref="lineNumberRefNew"
- data-qa-selector="new_diff_line_link"
:data-linenumber="line.new_line"
:href="line.lineHref"
@click="setHighlightedRow(line.lineCode)"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index 96946d0fd88..2d33926c8aa 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -193,7 +193,7 @@ export default {
v-show="shouldShowCommentButtonLeft"
ref="addDiffNoteButtonLeft"
type="button"
- class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note note-button js-add-diff-note-button"
:disabled="line.left.commentsDisabled"
:aria-label="addCommentTooltipLeft"
@click="handleCommentButton(line.left)"
@@ -251,7 +251,7 @@ export default {
v-show="shouldShowCommentButtonRight"
ref="addDiffNoteButtonRight"
type="button"
- class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note note-button js-add-diff-note-button"
:disabled="line.right.commentsDisabled"
:aria-label="addCommentTooltipRight"
@click="handleCommentButton(line.right)"
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 0163f508fea..f0e15983336 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -114,3 +114,20 @@ export const CONFLICT_THEIR = 'conflict_their';
export const CONFLICT_MARKER = 'conflict_marker';
export const CONFLICT_MARKER_OUR = 'conflict_marker_our';
export const CONFLICT_MARKER_THEIR = 'conflict_marker_their';
+
+// Tracking events
+export const TRACKING_CLICK_DIFF_VIEW_SETTING = 'i_code_review_click_diff_view_setting';
+export const TRACKING_DIFF_VIEW_INLINE = 'i_code_review_diff_view_inline';
+export const TRACKING_DIFF_VIEW_PARALLEL = 'i_code_review_diff_view_parallel';
+
+export const TRACKING_CLICK_FILE_BROWSER_SETTING = 'i_code_review_click_file_browser_setting';
+export const TRACKING_FILE_BROWSER_TREE = 'i_code_review_file_browser_tree_view';
+export const TRACKING_FILE_BROWSER_LIST = 'i_code_review_file_browser_list_view';
+
+export const TRACKING_CLICK_WHITESPACE_SETTING = 'i_code_review_click_whitespace_setting';
+export const TRACKING_WHITESPACE_SHOW = 'i_code_review_diff_show_whitespace';
+export const TRACKING_WHITESPACE_HIDE = 'i_code_review_diff_hide_whitespace';
+
+export const TRACKING_CLICK_SINGLE_FILE_SETTING = 'i_code_review_click_single_file_mode_setting';
+export const TRACKING_SINGLE_FILE_MODE = 'i_code_review_diff_single_file';
+export const TRACKING_MULTIPLE_FILES_MODE = 'i_code_review_diff_multiple_files';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 428faf693b0..d0730e18228 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,5 +1,6 @@
import Cookies from 'js-cookie';
import Vue from 'vue';
+import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
@@ -36,6 +37,18 @@ import {
DIFF_VIEW_FILE_BY_FILE,
DIFF_VIEW_ALL_FILES,
DIFF_FILE_BY_FILE_COOKIE_NAME,
+ TRACKING_CLICK_DIFF_VIEW_SETTING,
+ TRACKING_DIFF_VIEW_INLINE,
+ TRACKING_DIFF_VIEW_PARALLEL,
+ TRACKING_CLICK_FILE_BROWSER_SETTING,
+ TRACKING_FILE_BROWSER_TREE,
+ TRACKING_FILE_BROWSER_LIST,
+ TRACKING_CLICK_WHITESPACE_SETTING,
+ TRACKING_WHITESPACE_SHOW,
+ TRACKING_WHITESPACE_HIDE,
+ TRACKING_CLICK_SINGLE_FILE_SETTING,
+ TRACKING_SINGLE_FILE_MODE,
+ TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
import eventHub from '../event_hub';
import { isCollapsed } from '../utils/diff_file';
@@ -352,6 +365,11 @@ export const setInlineDiffViewType = ({ commit }) => {
Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
historyPushState(url);
+
+ if (window.gon?.features?.diffSettingsUsageData) {
+ api.trackRedisHllUserEvent(TRACKING_CLICK_DIFF_VIEW_SETTING);
+ api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_INLINE);
+ }
};
export const setParallelDiffViewType = ({ commit }) => {
@@ -360,6 +378,11 @@ export const setParallelDiffViewType = ({ commit }) => {
Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
historyPushState(url);
+
+ if (window.gon?.features?.diffSettingsUsageData) {
+ api.trackRedisHllUserEvent(TRACKING_CLICK_DIFF_VIEW_SETTING);
+ api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_PARALLEL);
+ }
};
export const showCommentForm = ({ commit }, { lineCode, fileHash }) => {
@@ -527,6 +550,16 @@ export const setRenderTreeList = ({ commit }, renderTreeList) => {
commit(types.SET_RENDER_TREE_LIST, renderTreeList);
localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList);
+
+ if (window.gon?.features?.diffSettingsUsageData) {
+ api.trackRedisHllUserEvent(TRACKING_CLICK_FILE_BROWSER_SETTING);
+
+ if (renderTreeList) {
+ api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_LIST);
+ }
+ }
};
export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => {
@@ -540,6 +573,16 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals
}
notesEventHub.$emit('refetchDiffData');
+
+ if (window.gon?.features?.diffSettingsUsageData) {
+ api.trackRedisHllUserEvent(TRACKING_CLICK_WHITESPACE_SETTING);
+
+ if (showWhitespace) {
+ api.trackRedisHllUserEvent(TRACKING_WHITESPACE_SHOW);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_WHITESPACE_HIDE);
+ }
+ }
};
export const toggleFileFinder = ({ commit }, visible) => {
@@ -754,6 +797,16 @@ export const setFileByFile = ({ state, commit }, { fileByFile }) => {
commit(types.SET_FILE_BY_FILE, fileByFile);
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
+ if (window.gon?.features?.diffSettingsUsageData) {
+ api.trackRedisHllUserEvent(TRACKING_CLICK_SINGLE_FILE_SETTING);
+
+ if (fileByFile) {
+ api.trackRedisHllUserEvent(TRACKING_SINGLE_FILE_MODE);
+ } else {
+ api.trackRedisHllUserEvent(TRACKING_MULTIPLE_FILES_MODE);
+ }
+ }
+
return axios
.put(state.endpointUpdateUser, {
view_diffs_file_by_file: fileByFile,
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index dec3f87b03e..0a9623c13a3 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -170,3 +170,6 @@ export function suggestionCommitMessage(state, _, rootState) {
},
});
}
+
+export const isVirtualScrollingEnabled = (state) =>
+ !state.viewDiffsFileByFile && window.gon?.features?.diffsVirtualScrolling;
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index 01811e60caa..673ec821b58 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -7,6 +7,9 @@ export const selectedTargetIndex = (state) =>
export const selectedSourceIndex = (state) => state.mergeRequestDiff.version_index;
+export const selectedContextCommitsDiffs = (state) =>
+ state.contextCommitsDiff && state.contextCommitsDiff.showing_context_commits_diff;
+
export const diffCompareDropdownTargetVersions = (state, getters) => {
// startVersion only exists if the user has selected a version other
// than "base" so if startVersion is null then base must be selected
@@ -58,7 +61,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
export const diffCompareDropdownSourceVersions = (state, getters) => {
// Appended properties here are to make the compare_dropdown_layout easier to reason about
- return state.mergeRequestDiffs.map((v, i) => {
+ const versions = state.mergeRequestDiffs.map((v, i) => {
const isLatestVersion = i === 0;
return {
@@ -69,7 +72,20 @@ export const diffCompareDropdownSourceVersions = (state, getters) => {
versionName: isLatestVersion
? __('latest version')
: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }),
- selected: v.version_index === getters.selectedSourceIndex,
+ selected:
+ v.version_index === getters.selectedSourceIndex && !getters.selectedContextCommitsDiffs,
};
});
+
+ const { contextCommitsDiff } = state;
+ if (contextCommitsDiff) {
+ versions.push({
+ href: contextCommitsDiff.diffs_path,
+ commitsText: n__(`%d commit`, `%d commits`, contextCommitsDiff.commits_count),
+ versionName: __('previously merged commits'),
+ selected: getters.selectedContextCommitsDiffs,
+ addDivider: state.mergeRequestDiffs.length > 0,
+ });
+ }
+ return versions;
};
diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js
index 7e6fde320d2..a96c1207a04 100644
--- a/app/assets/javascripts/diffs/utils/diff_file.js
+++ b/app/assets/javascripts/diffs/utils/diff_file.js
@@ -1,4 +1,5 @@
import { truncateSha } from '~/lib/utils/text_utility';
+import { uuids } from '~/lib/utils/uuids';
import {
DIFF_FILE_SYMLINK_MODE,
@@ -7,7 +8,6 @@ import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
} from '../constants';
import { getDerivedMergeRequestInformation } from './merge_request';
-import { uuids } from './uuids';
function fileSymlinkInformation(file, fileList) {
const duplicates = fileList.filter((iteratedFile) => iteratedFile.file_hash === file.file_hash);
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 1f57d73d3d3..aa223270f2c 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,6 +2,7 @@
import dateFormat from 'dateformat';
import $ from 'jquery';
import Pikaday from 'pikaday';
+import initDatePicker from '~/behaviors/date_picker';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __ } from '~/locale';
import boardsStore from './boards/stores/boards_store';
@@ -168,40 +169,10 @@ class DueDateSelect {
export default class DueDateSelectors {
constructor() {
- this.initMilestoneDatePicker();
+ initDatePicker();
this.initIssuableSelect();
}
// eslint-disable-next-line class-methods-use-this
- initMilestoneDatePicker() {
- $('.datepicker').each(function initPikadayMilestone() {
- const $datePicker = $(this);
- const datePickerVal = $datePicker.val();
-
- const calendar = new Pikaday({
- field: $datePicker.get(0),
- theme: 'gitlab-theme animate-picker',
- format: 'yyyy-mm-dd',
- container: $datePicker.parent().get(0),
- parse: (dateString) => parsePikadayDate(dateString),
- toString: (date) => pikadayToString(date),
- onSelect(dateText) {
- $datePicker.val(calendar.toString(dateText));
- },
- firstDay: gon.first_day_of_week,
- });
-
- calendar.setDate(parsePikadayDate(datePickerVal));
-
- $datePicker.data('pikaday', calendar);
- });
-
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
- e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
- calendar.setDate(null);
- });
- }
- // eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
const $loading = $('.js-issuable-update .due_date')
.find('.block-loading')
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 79beb3a4857..249888ede9b 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,10 +1,10 @@
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
-import { uuids } from '~/diffs/utils/uuids';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import languages from '~/ide/lib/languages';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import { uuids } from '~/lib/utils/uuids';
import {
EDITOR_LITE_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
diff --git a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js
index 3d4f08131c1..05a020bd958 100644
--- a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js
@@ -1,4 +1,5 @@
import { Range } from 'monaco-editor';
+import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION, EDITOR_TYPE_CODE } from '../constants';
const hashRegexp = new RegExp('#?L', 'g');
@@ -23,11 +24,18 @@ export class EditorLiteExtension {
if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) {
EditorLiteExtension.setupLineLinking(instance);
}
+ EditorLiteExtension.deferRerender(instance);
} else if (Object.entries(options).length) {
throw new Error(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION);
}
}
+ static deferRerender(instance) {
+ waitForCSSLoaded(() => {
+ instance.layout();
+ });
+ }
+
static highlightLines(instance) {
const { hash } = window.location;
if (!hash) {
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 71cabe80529..e08d294b8c5 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -79,6 +79,7 @@ export default {
:toggle-class="toggleClass"
:boundary="getBoundaryElement()"
menu-class="dropdown-extended-height"
+ category="tertiary"
no-flip
right
lazy
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
index 5b4d1afc9d0..69c81c35bd4 100644
--- a/app/assets/javascripts/ensure_data.js
+++ b/app/assets/javascripts/ensure_data.js
@@ -3,8 +3,8 @@ import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __ } from '~/locale';
-const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
-const ERROR_FETCHING_DATA_DESCRIPTION = __(
+export const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
+export const ERROR_FETCHING_DATA_DESCRIPTION = __(
'Please try and refresh the page. If the problem persists please contact support.',
);
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index cbce887f491..f82d3065ca5 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -188,15 +188,37 @@ export default {
</div>
<template v-else>
- <div
- is="environment-item"
- v-for="(children, index) in model.children"
- :key="`env-item-${i}-${index}`"
- :model="children"
- :can-read-environment="canReadEnvironment"
- :table-data="tableData"
- data-qa-selector="environment_item"
- />
+ <template v-for="(child, index) in model.children">
+ <div
+ is="environment-item"
+ :key="`environment-row-${i}-${index}`"
+ :model="child"
+ :can-read-environment="canReadEnvironment"
+ :table-data="tableData"
+ data-qa-selector="environment_item"
+ />
+
+ <div
+ v-if="shouldRenderDeployBoard(child)"
+ :key="`deploy-board-row-${i}-${index}`"
+ class="js-deploy-board-row"
+ >
+ <div class="deploy-board-container">
+ <deploy-board
+ :deploy-board-data="child.deployBoardData"
+ :is-loading="child.isLoadingDeployBoard"
+ :is-empty="child.isEmptyDeployBoard"
+ :logs-path="child.logs_path"
+ @changeCanaryWeight="changeCanaryWeight(child, $event)"
+ />
+ </div>
+ </div>
+ <environment-alert
+ v-if="shouldRenderAlert(model)"
+ :key="`alert-row-${i}-${index}`"
+ :environment="child"
+ />
+ </template>
<div :key="`sub-div-${i}`">
<div class="text-center gl-mt-3">
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index f7fdbb03f04..a67e44b3348 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -185,6 +185,8 @@ export default class EnvironmentsStore {
updated.isChildren = true;
+ updated = setDeployBoard(env, updated);
+
return updated;
});
diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js
index 89457da0614..3330edd8830 100644
--- a/app/assets/javascripts/environments/stores/helpers.js
+++ b/app/assets/javascripts/environments/stores/helpers.js
@@ -4,7 +4,7 @@
*/
export const setDeployBoard = (oldEnvironmentState, environment) => {
let parsedEnvironment = environment;
- if (environment.size === 1 && environment.rollout_status) {
+ if (!environment.isFolder && environment.rollout_status) {
parsedEnvironment = {
...environment,
hasDeployBoard: true,
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 971eb21ee3b..d188574e721 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -1,11 +1,17 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import ErrorTrackingForm from './error_tracking_form.vue';
import ProjectDropdown from './project_dropdown.vue';
export default {
- components: { ProjectDropdown, ErrorTrackingForm, GlButton },
+ components: {
+ ErrorTrackingForm,
+ GlButton,
+ GlFormCheckbox,
+ GlFormGroup,
+ ProjectDropdown,
+ },
props: {
initialApiHost: {
type: String,
@@ -66,18 +72,18 @@ export default {
<template>
<div>
- <div class="form-check form-group">
- <input
+ <gl-form-group
+ :label="s__('ErrorTracking|Enable error tracking')"
+ label-for="error-tracking-enabled"
+ >
+ <gl-form-checkbox
id="error-tracking-enabled"
:checked="enabled"
- class="form-check-input"
- type="checkbox"
- @change="updateEnabled($event.target.checked)"
- />
- <label class="form-check-label" for="error-tracking-enabled">{{
- s__('ErrorTracking|Active')
- }}</label>
- </div>
+ @change="updateEnabled($event)"
+ >
+ {{ s__('ErrorTracking|Active') }}
+ </gl-form-checkbox>
+ </gl-form-group>
<error-tracking-form />
<div class="form-group">
<project-dropdown
@@ -95,7 +101,7 @@ export default {
<gl-button
:disabled="settingsLoading"
class="js-error-tracking-button"
- variant="success"
+ variant="confirm"
@click="handleSubmit"
>
{{ __('Save changes') }}
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index 4df324b396c..da942dbd0ae 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -70,7 +70,7 @@ export default {
v-show="connectSuccessful"
class="js-error-tracking-connect-success gl-ml-2 text-success align-middle"
:aria-label="__('Projects Successfully Retrieved')"
- name="check-circle"
+ name="check"
/>
</div>
</div>
diff --git a/app/assets/javascripts/experimentation/components/experiment.vue b/app/assets/javascripts/experimentation/components/gitlab_experiment.vue
index 294dbf77991..294dbf77991 100644
--- a/app/assets/javascripts/experimentation/components/experiment.vue
+++ b/app/assets/javascripts/experimentation/components/gitlab_experiment.vue
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index 572907f226d..e572280a62c 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -1,11 +1,20 @@
// This file only applies to use of experiments through https://gitlab.com/gitlab-org/gitlab-experiment
import { get } from 'lodash';
-import { DEFAULT_VARIANT, CANDIDATE_VARIANT } from './constants';
+import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
export function getExperimentData(experimentName) {
return get(window, ['gon', 'experiment', experimentName]);
}
+export function getExperimentContexts(...experimentNames) {
+ return experimentNames
+ .map((name) => {
+ const data = getExperimentData(name);
+ return data && { schema: TRACKING_CONTEXT_SCHEMA, data };
+ })
+ .filter((context) => context);
+}
+
export function isExperimentVariant(experimentName, variantName) {
return getExperimentData(experimentName)?.variant === variantName;
}
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index 115d68de5c9..9aa1accb0f2 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -35,8 +35,9 @@ export default {
inject: {
newUserListPath: { default: '' },
newFeatureFlagPath: { default: '' },
- canUserConfigure: { required: true },
- featureFlagsLimitExceeded: { required: true },
+ canUserConfigure: {},
+ featureFlagsLimitExceeded: {},
+ featureFlagsLimit: {},
},
data() {
const scope = getParameterByName('scope') || SCOPES.FEATURE_FLAG_SCOPE;
diff --git a/app/assets/javascripts/feature_flags/index.js b/app/assets/javascripts/feature_flags/index.js
index a92805d5d85..d2371a2aa8b 100644
--- a/app/assets/javascripts/feature_flags/index.js
+++ b/app/assets/javascripts/feature_flags/index.js
@@ -24,6 +24,7 @@ export default () => {
newFeatureFlagPath,
newUserListPath,
featureFlagsLimitExceeded,
+ featureFlagsLimit,
} = el.dataset;
return new Vue({
@@ -40,7 +41,8 @@ export default () => {
canUserConfigure: canUserAdminFeatureFlag !== undefined,
newFeatureFlagPath,
newUserListPath,
- featureFlagsLimitExceeded,
+ featureFlagsLimitExceeded: featureFlagsLimitExceeded !== undefined,
+ featureFlagsLimit,
},
render(createElement) {
return createElement(FeatureFlagsComponent);
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index 0da8cd0ad83..f933338514a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -50,7 +50,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
];
const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase());
- if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) {
+ if (!dropdownToken?.hideNotEqual) {
dropdownData.push({
tag: 'not-equal',
type: 'string',
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 2bec39ff4d8..7a79f8f5bfc 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -9,6 +9,10 @@ const FLASH_TYPES = {
WARNING: 'warning',
};
+const getCloseEl = (flashEl) => {
+ return flashEl.querySelector('.js-close-icon');
+};
+
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
Object.assign(flashEl.style, {
@@ -56,9 +60,7 @@ const createFlashEl = (message, type) => `
`;
const removeFlashClickListener = (flashEl, fadeTransition) => {
- flashEl
- .querySelector('.js-close-icon')
- .addEventListener('click', () => hideFlash(flashEl, fadeTransition));
+ getCloseEl(flashEl).addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
/*
@@ -114,6 +116,10 @@ const createFlash = function createFlash({
if (captureError && error) Sentry.captureException(error);
+ flashContainer.close = () => {
+ getCloseEl(flashEl).click();
+ };
+
return flashContainer;
};
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 69f89aa3857..e103949b86a 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,7 +1,11 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { mapState, mapActions, mapGetters } from 'vuex';
import AccessorUtilities from '~/lib/utils/accessor';
+import {
+ mapVuexModuleState,
+ mapVuexModuleActions,
+ mapVuexModuleGetters,
+} from '~/lib/utils/vuex_module_mappers';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import eventHub from '../event_hub';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
@@ -16,6 +20,7 @@ export default {
GlLoadingIcon,
},
mixins: [frequentItemsMixin],
+ inject: ['vuexModule'],
props: {
currentUserName: {
type: String,
@@ -27,8 +32,13 @@ export default {
},
},
computed: {
- ...mapState(['searchQuery', 'isLoadingItems', 'isFetchFailed', 'items']),
- ...mapGetters(['hasSearchQuery']),
+ ...mapVuexModuleState((vm) => vm.vuexModule, [
+ 'searchQuery',
+ 'isLoadingItems',
+ 'isFetchFailed',
+ 'items',
+ ]),
+ ...mapVuexModuleGetters((vm) => vm.vuexModule, ['hasSearchQuery']),
translations() {
return this.getTranslations(['loadingMessage', 'header']);
},
@@ -56,7 +66,11 @@ export default {
eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
},
methods: {
- ...mapActions(['setNamespace', 'setStorageKey', 'fetchFrequentItems']),
+ ...mapVuexModuleActions((vm) => vm.vuexModule, [
+ 'setNamespace',
+ 'setStorageKey',
+ 'fetchFrequentItems',
+ ]),
dropdownOpenHandler() {
if (this.searchQuery === '' || isMobile()) {
this.fetchFrequentItems();
@@ -100,15 +114,16 @@ export default {
</script>
<template>
- <div>
- <frequent-items-search-input :namespace="namespace" />
+ <div class="gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch gl-h-full">
+ <frequent-items-search-input :namespace="namespace" data-testid="frequent-items-search-input" />
<gl-loading-icon
v-if="isLoadingItems"
:label="translations.loadingMessage"
size="lg"
class="loading-animation prepend-top-20"
+ data-testid="loading"
/>
- <div v-if="!isLoadingItems && !hasSearchQuery" class="section-header">
+ <div v-if="!isLoadingItems && !hasSearchQuery" class="section-header" data-testid="header">
{{ translations.header }}
</div>
<frequent-items-list
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index 6feeb5f03ad..1da0b88c9e9 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -59,7 +59,11 @@ export default {
<template>
<div class="frequent-items-list-container">
<ul ref="frequentItemsList" class="list-unstyled">
- <li v-if="isListEmpty" :class="{ 'section-failure': isFetchFailed }" class="section-empty">
+ <li
+ v-if="isListEmpty"
+ :class="{ 'section-failure': isFetchFailed }"
+ class="section-empty gl-mb-3"
+ >
{{ listEmptyMessage }}
</li>
<frequent-items-list-item
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 6f17e6a5282..c2f77cc8bc4 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
-import { mapState } from 'vuex';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
+import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
import Tracking from '~/tracking';
import Identicon from '~/vue_shared/components/identicon.vue';
@@ -13,6 +13,7 @@ export default {
Identicon,
},
mixins: [trackingMixin],
+ inject: ['vuexModule'],
props: {
matcher: {
type: String,
@@ -42,7 +43,7 @@ export default {
},
},
computed: {
- ...mapState(['dropdownType']),
+ ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index b0972246e70..fa14ee15cf3 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -1,7 +1,7 @@
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
-import { mapActions, mapState } from 'vuex';
+import { mapVuexModuleActions, mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
import Tracking from '~/tracking';
import frequentItemsMixin from './frequent_items_mixin';
@@ -12,13 +12,14 @@ export default {
GlSearchBoxByType,
},
mixins: [frequentItemsMixin, trackingMixin],
+ inject: ['vuexModule'],
data() {
return {
searchQuery: '',
};
},
computed: {
- ...mapState(['dropdownType']),
+ ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType']),
translations() {
return this.getTranslations(['searchInputPlaceholder']);
},
@@ -32,7 +33,7 @@ export default {
}, 500),
},
methods: {
- ...mapActions(['setSearchQuery']),
+ ...mapVuexModuleActions((vm) => vm.vuexModule, ['setSearchQuery']),
},
};
</script>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
index 9bc17f5ef4f..9e1dcf70aa5 100644
--- a/app/assets/javascripts/frequent_items/constants.js
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -36,3 +36,17 @@ export const TRANSLATION_KEYS = {
searchInputPlaceholder: s__('GroupsDropdown|Search your groups'),
},
};
+
+export const FREQUENT_ITEMS_PROJECTS = {
+ namespace: 'projects',
+ key: 'project',
+ vuexModule: 'frequentProjects',
+};
+
+export const FREQUENT_ITEMS_GROUPS = {
+ namespace: 'groups',
+ key: 'group',
+ vuexModule: 'frequentGroups',
+};
+
+export const FREQUENT_ITEMS_DROPDOWNS = [FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS];
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index eb8a404e8a5..f1540ffac28 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -1,25 +1,20 @@
import $ from 'jquery';
import Vue from 'vue';
+import Vuex from 'vuex';
import { createStore } from '~/frequent_items/store';
+import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
import Translate from '~/vue_shared/translate';
+import { FREQUENT_ITEMS_DROPDOWNS } from './constants';
import eventHub from './event_hub';
+Vue.use(Vuex);
Vue.use(Translate);
-const frequentItemDropdowns = [
- {
- namespace: 'projects',
- key: 'project',
- },
- {
- namespace: 'groups',
- key: 'group',
- },
-];
-
export default function initFrequentItemDropdowns() {
- frequentItemDropdowns.forEach((dropdown) => {
- const { namespace, key } = dropdown;
+ const store = createStore();
+
+ FREQUENT_ITEMS_DROPDOWNS.forEach((dropdown) => {
+ const { namespace, key, vuexModule } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
const navEl = document.getElementById(`nav-${namespace}-dropdown`);
@@ -29,9 +24,6 @@ export default function initFrequentItemDropdowns() {
return;
}
- const dropdownType = namespace;
- const store = createStore({ dropdownType });
-
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
@@ -55,13 +47,23 @@ export default function initFrequentItemDropdowns() {
};
},
render(createElement) {
- return createElement(FrequentItems, {
- props: {
- namespace,
- currentUserName: this.currentUserName,
- currentItem: this.currentItem,
+ return createElement(
+ VuexModuleProvider,
+ {
+ props: {
+ vuexModule,
+ },
},
- });
+ [
+ createElement(FrequentItems, {
+ props: {
+ namespace,
+ currentUserName: this.currentUserName,
+ currentItem: this.currentItem,
+ },
+ }),
+ ],
+ );
},
});
})
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
index 83176d69802..1faacff84e5 100644
--- a/app/assets/javascripts/frequent_items/store/index.js
+++ b/app/assets/javascripts/frequent_items/store/index.js
@@ -1,17 +1,28 @@
-import Vue from 'vue';
import Vuex from 'vuex';
+import { FREQUENT_ITEMS_DROPDOWNS } from '../constants';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
-Vue.use(Vuex);
+export const createFrequentItemsModule = (initState = {}) => ({
+ namespaced: true,
+ actions,
+ getters,
+ mutations,
+ state: state(initState),
+});
-export const createStore = (initState = {}) => {
- return new Vuex.Store({
- actions,
- getters,
- mutations,
- state: state(initState),
- });
+export const createStoreOptions = () => ({
+ modules: FREQUENT_ITEMS_DROPDOWNS.reduce(
+ (acc, { namespace, vuexModule }) =>
+ Object.assign(acc, {
+ [vuexModule]: createFrequentItemsModule({ dropdownType: namespace }),
+ }),
+ {},
+ ),
+});
+
+export const createStore = () => {
+ return new Vuex.Store(createStoreOptions());
};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 22f88b1caa7..470c785f7e4 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -238,10 +238,13 @@ class GfmAutoComplete {
const MEMBER_COMMAND = {
ASSIGN: '/assign',
UNASSIGN: '/unassign',
+ ASSIGN_REVIEWER: '/assign_reviewer',
+ UNASSIGN_REVIEWER: '/unassign_reviewer',
REASSIGN: '/reassign',
CC: '/cc',
};
let assignees = [];
+ let reviewers = [];
let command = '';
// Team Members
@@ -286,9 +289,11 @@ class GfmAutoComplete {
return null;
});
- // Cache assignees list for easier filtering later
+ // Cache assignees & reviewers list for easier filtering later
assignees =
SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
+ reviewers =
+ SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || [];
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
@@ -309,6 +314,12 @@ class GfmAutoComplete {
} else if (command === MEMBER_COMMAND.UNASSIGN) {
// Only include members which are assigned to Issuable currently
return data.filter((member) => assignees.includes(member.search));
+ } else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
+ // Only include members which are not assigned as a reviewer to Issuable currently
+ return data.filter((member) => !reviewers.includes(member.search));
+ } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
+ // Only include members which are not assigned as a reviewer to Issuable currently
+ return data.filter((member) => reviewers.includes(member.search));
}
return data;
@@ -823,10 +834,10 @@ GfmAutoComplete.Members = {
const lowercaseQuery = query.toLowerCase();
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
- return sortBy(members, [
+ return sortBy(
+ members.filter((member) => nameOrUsernameIncludes(member, lowercaseQuery)),
(member) => (nameOrUsernameStartsWith(member, lowercaseQuery) ? -1 : 0),
- (member) => (nameOrUsernameIncludes(member, lowercaseQuery) ? -1 : 0),
- ]);
+ );
},
};
GfmAutoComplete.Labels = {
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
index 101633ef7a7..41e7ed98c78 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
@@ -9,6 +9,7 @@ fragment AlertListItem on AlertManagementAlert {
iid
state
title
+ webUrl
}
assignees {
nodes {
diff --git a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
new file mode 100644
index 00000000000..6ed3be84cd8
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
@@ -0,0 +1,10 @@
+fragment TimelogFragment on Timelog {
+ timeSpent
+ user {
+ name
+ }
+ spentAt
+ note {
+ body
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql
new file mode 100644
index 00000000000..2b831bf1338
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql
@@ -0,0 +1,9 @@
+mutation dismissUserCallout($input: UserCalloutCreateInput!) {
+ userCalloutCreate(input: $input) {
+ errors
+ userCallout {
+ dismissedAt
+ featureName
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
index 747cea6a46e..402d9a07c53 100644
--- a/app/assets/javascripts/groups/components/invite_members_banner.vue
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -1,5 +1,6 @@
<script>
import { GlBanner } from '@gitlab/ui';
+import eventHub from '~/invite_members/event_hub';
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -11,7 +12,7 @@ export default {
GlBanner,
},
mixins: [trackingMixin],
- inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'],
+ inject: ['svgPath', 'isDismissedKey', 'trackLabel'],
data() {
return {
isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
@@ -20,11 +21,6 @@ export default {
},
};
},
- created() {
- this.$nextTick(() => {
- this.addTrackingAttributesToButton();
- });
- },
mounted() {
this.trackOnShow();
},
@@ -39,15 +35,12 @@ export default {
if (!this.isDismissed) this.track(this.$options.displayEvent);
});
},
- addTrackingAttributesToButton() {
- if (this.$refs.banner === undefined) return;
-
- const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`);
-
- if (button) {
- button.setAttribute('data-track-event', this.$options.buttonClickEvent);
- button.setAttribute('data-track-label', this.trackLabel);
- }
+ openModal() {
+ eventHub.$emit('openModal', {
+ inviteeType: 'members',
+ source: this.$options.openModalSource,
+ });
+ this.track(this.$options.buttonClickEvent);
},
},
i18n: {
@@ -59,6 +52,7 @@ export default {
},
displayEvent: 'invite_members_banner_displayed',
buttonClickEvent: 'invite_members_banner_button_clicked',
+ openModalSource: 'invite_members_banner',
dismissEvent: 'invite_members_banner_dismissed',
};
</script>
@@ -70,8 +64,8 @@ export default {
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:svg-path="svgPath"
- :button-link="inviteMembersPath"
@close="handleClose"
+ @primary="openModal"
>
<p>{{ $options.i18n.body }}</p>
</gl-banner>
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 4fed7f555f6..c2ef6414716 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -45,7 +45,6 @@ function initStatusTriggers() {
defaultEmoji,
currentMessage,
currentAvailability,
- canSetUserAvailability,
currentClearStatusAfter,
} = setStatusModalWrapperEl.dataset;
@@ -54,7 +53,6 @@ function initStatusTriggers() {
defaultEmoji,
currentMessage,
currentAvailability,
- canSetUserAvailability,
currentClearStatusAfter,
};
},
@@ -64,7 +62,6 @@ function initStatusTriggers() {
defaultEmoji,
currentMessage,
currentAvailability,
- canSetUserAvailability,
currentClearStatusAfter,
} = this;
@@ -74,7 +71,6 @@ function initStatusTriggers() {
defaultEmoji,
currentMessage,
currentAvailability,
- canSetUserAvailability,
currentClearStatusAfter,
},
});
diff --git a/app/assets/javascripts/help/help.js b/app/assets/javascripts/help/help.js
deleted file mode 100644
index f5333042bb8..00000000000
--- a/app/assets/javascripts/help/help.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// We will render the icons list here
-
-import $ from 'jquery';
-
-export default () => {
- if ($('#user-content-gitlab-icons').length > 0) {
- const $iconsHeader = $('#user-content-gitlab-icons');
- const $iconsList = $('<div id="iconsList">ICONS</div>');
- $($iconsList).insertAfter($iconsHeader.parent());
- }
-};
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue
index 35e2f99cb6a..bdfcff3136b 100644
--- a/app/assets/javascripts/ide/components/branches/item.vue
+++ b/app/assets/javascripts/ide/components/branches/item.vue
@@ -34,7 +34,7 @@ export default {
<template>
<a :href="branchHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" use-deprecated-sizes />
+ <gl-icon v-if="isActive" :size="16" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.name }} </strong>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 273d8d972f7..fcc900bbc96 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -76,8 +76,9 @@ export default {
:value="$options.commitToCurrentBranch"
:disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip"
+ data-qa-selector="commit_to_current_branch_radio_container"
>
- <span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio">
+ <span class="ide-option-label">
<gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
<template #branchName>
<strong class="monospace">{{ currentBranchText }}</strong>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 039b4a54b26..870355e884e 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -64,6 +64,7 @@ export default {
:disabled="disabled"
type="radio"
name="commit-action"
+ data-qa-selector="commit_type_radio"
@change="updateCommitAction($event.target.value)"
/>
<span class="gl-ml-3">
diff --git a/app/assets/javascripts/ide/components/file_alert.vue b/app/assets/javascripts/ide/components/file_alert.vue
new file mode 100644
index 00000000000..2a894596bf4
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_alert.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { getAlert } from '../lib/alerts';
+
+export default {
+ components: {
+ GlAlert,
+ },
+ props: {
+ alertKey: {
+ type: Symbol,
+ required: true,
+ },
+ },
+ computed: {
+ alert() {
+ return getAlert(this.alertKey);
+ },
+ },
+};
+</script>
+<template>
+ <gl-alert v-bind="alert.props" @dismiss="alert.dismiss($store)">
+ <component :is="alert.message" />
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b57dcd4276c..bf2af9ffd49 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,4 +1,5 @@
<script>
+import { debounce } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex';
import {
EDITOR_TYPE_DIFF,
@@ -34,11 +35,13 @@ import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
+import FileAlert from './file_alert.vue';
import FileTemplatesBar from './file_templates/bar.vue';
export default {
name: 'RepoEditor',
components: {
+ FileAlert,
ContentViewer,
DiffViewer,
FileTemplatesBar,
@@ -57,6 +60,7 @@ export default {
globalEditor: null,
modelManager: new ModelManager(),
isEditorLoading: true,
+ unwatchCiYaml: null,
};
},
computed: {
@@ -74,6 +78,7 @@ export default {
'currentProjectId',
]),
...mapGetters([
+ 'getAlert',
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
@@ -82,6 +87,9 @@ export default {
'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
+ alertKey() {
+ return this.getAlert(this.file);
+ },
fileEditor() {
return getFileEditorOrDefault(this.fileEditors, this.file.path);
},
@@ -136,6 +144,16 @@ export default {
},
},
watch: {
+ 'file.name': {
+ handler() {
+ this.stopWatchingCiYaml();
+
+ if (this.file.name === '.gitlab-ci.yml') {
+ this.startWatchingCiYaml();
+ }
+ },
+ immediate: true,
+ },
file(newVal, oldVal) {
if (oldVal.pending) {
this.removePendingTab(oldVal);
@@ -216,6 +234,7 @@ export default {
'removePendingTab',
'triggerFilesChange',
'addTempImage',
+ 'detectGitlabCiFileAlerts',
]),
...mapActions('editor', ['updateFileEditor']),
initEditor() {
@@ -422,6 +441,18 @@ export default {
this.updateFileEditor({ path: this.file.path, data });
},
+ startWatchingCiYaml() {
+ this.unwatchCiYaml = this.$watch(
+ 'file.content',
+ debounce(this.detectGitlabCiFileAlerts, 500),
+ );
+ },
+ stopWatchingCiYaml() {
+ if (this.unwatchCiYaml) {
+ this.unwatchCiYaml();
+ this.unwatchCiYaml = null;
+ }
+ },
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@@ -439,9 +470,8 @@ export default {
role="button"
data-testid="edit-tab"
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
+ >{{ __('Edit') }}</a
>
- {{ __('Edit') }}
- </a>
</li>
<li v-if="previewMode" :class="previewTabCSS">
<a
@@ -454,7 +484,8 @@ export default {
</li>
</ul>
</div>
- <file-templates-bar v-if="showFileTemplatesBar(file.name)" />
+ <file-alert v-if="alertKey" :alert-key="alertKey" />
+ <file-templates-bar v-else-if="showFileTemplatesBar(file.name)" />
<div
v-show="showEditor"
ref="editor"
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 2ce5bf7e271..7109c45a3fe 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -56,11 +56,12 @@ export function initIde(el, options = {}) {
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null,
});
- this.setInitialData({
+ this.init({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
+ environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
});
},
beforeDestroy() {
@@ -68,7 +69,7 @@ export function initIde(el, options = {}) {
this.$emit('destroy');
},
methods: {
- ...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']),
+ ...mapActions(['setEmptyStateSvgs', 'setLinks', 'init']),
},
render(createElement) {
return createElement(rootComponent);
diff --git a/app/assets/javascripts/ide/lib/alerts/environments.vue b/app/assets/javascripts/ide/lib/alerts/environments.vue
new file mode 100644
index 00000000000..ac9a3c3f82c
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/alerts/environments.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { __ } from '~/locale';
+
+export default {
+ components: { GlSprintf, GlLink },
+ message: __(
+ "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}",
+ ),
+ computed: {
+ helpLink() {
+ return helpPagePath('ci/environments/index.md');
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ <gl-sprintf :message="$options.message">
+ <template #link="{ content }">
+ <gl-link
+ :href="helpLink"
+ target="_blank"
+ data-track-action="click_link"
+ data-track-experiment="in_product_guidance_environments_webide"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js
new file mode 100644
index 00000000000..c9db9779b1f
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/alerts/index.js
@@ -0,0 +1,20 @@
+import { leftSidebarViews } from '../../constants';
+import EnvironmentsMessage from './environments.vue';
+
+const alerts = [
+ {
+ key: Symbol('ALERT_ENVIRONMENT'),
+ show: (state, file) =>
+ state.currentActivityView === leftSidebarViews.commit.name &&
+ file.path === '.gitlab-ci.yml' &&
+ state.environmentsGuidanceAlertDetected &&
+ !state.environmentsGuidanceAlertDismissed,
+ props: { variant: 'tip' },
+ dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'),
+ message: EnvironmentsMessage,
+ },
+];
+
+export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key;
+
+export const getAlert = (key) => alerts.find((x) => x.key === key);
diff --git a/app/assets/javascripts/ide/messages.js b/app/assets/javascripts/ide/messages.js
index 189226ef835..fe8eba823a8 100644
--- a/app/assets/javascripts/ide/messages.js
+++ b/app/assets/javascripts/ide/messages.js
@@ -1,11 +1,11 @@
import { s__ } from '~/locale';
export const MSG_CANNOT_PUSH_CODE_SHOULD_FORK = s__(
- 'WebIDE|You need permission to edit files directly in this project. Fork this project to make your changes and submit a merge request.',
+ 'WebIDE|You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
);
export const MSG_CANNOT_PUSH_CODE_GO_TO_FORK = s__(
- 'WebIDE|You need permission to edit files directly in this project. Go to your fork to make changes and submit a merge request.',
+ 'WebIDE|You can’t edit files directly in this project. Go to your fork and submit a merge request with your changes.',
);
export const MSG_CANNOT_PUSH_CODE = s__(
@@ -13,7 +13,7 @@ export const MSG_CANNOT_PUSH_CODE = s__(
);
export const MSG_CANNOT_PUSH_UNSIGNED = s__(
- 'WebIDE|This project does not accept unsigned commits. You will not be able to commit your changes through the Web IDE.',
+ 'WebIDE|This project does not accept unsigned commits. You can’t commit changes through the Web IDE.',
);
export const MSG_CANNOT_PUSH_UNSIGNED_SHORT = s__(
diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js
index 89dda187360..c8c1031c0f3 100644
--- a/app/assets/javascripts/ide/services/gql.js
+++ b/app/assets/javascripts/ide/services/gql.js
@@ -18,3 +18,4 @@ const getClient = memoize(() =>
);
export const query = (...args) => getClient().query(...args);
+export const mutate = (...args) => getClient().mutate(...args);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 0aa08323d13..6bd28cd4fb6 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,8 +1,10 @@
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
import Api from '~/api';
+import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import { query } from './gql';
+import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import { query, mutate } from './gql';
const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
@@ -101,4 +103,16 @@ export default {
const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`;
return axios.post(url);
},
+ getCiConfig(projectPath, content) {
+ return query({
+ query: ciConfig,
+ variables: { projectPath, content },
+ }).then(({ data }) => data.ciConfig);
+ },
+ dismissUserCallout(name) {
+ return mutate({
+ mutation: dismissUserCallout,
+ variables: { input: { featureName: name } },
+ }).then(({ data }) => data);
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index bf94f9d31c8..062dc150805 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -17,7 +17,7 @@ import * as types from './mutation_types';
export const redirectToUrl = (self, url) => visitUrl(url);
-export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
+export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path));
@@ -316,3 +316,4 @@ export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+export * from './actions/alert';
diff --git a/app/assets/javascripts/ide/stores/actions/alert.js b/app/assets/javascripts/ide/stores/actions/alert.js
new file mode 100644
index 00000000000..4c33dc19520
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/alert.js
@@ -0,0 +1,18 @@
+import service from '../../services';
+import {
+ DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
+ DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
+} from '../mutation_types';
+
+export const detectGitlabCiFileAlerts = ({ dispatch }, content) =>
+ dispatch('detectEnvironmentsGuidance', content);
+
+export const detectEnvironmentsGuidance = ({ commit, state }, content) =>
+ service.getCiConfig(state.currentProjectId, content).then((data) => {
+ commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages);
+ });
+
+export const dismissEnvironmentsGuidance = ({ commit }) =>
+ service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => {
+ commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT);
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index e8b1a0ea494..3c02b1d1da7 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => {
fileMatch: [`*${path}`],
};
};
+
+export * from './getters/alert';
diff --git a/app/assets/javascripts/ide/stores/getters/alert.js b/app/assets/javascripts/ide/stores/getters/alert.js
new file mode 100644
index 00000000000..714e2d89b4f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/getters/alert.js
@@ -0,0 +1,3 @@
+import { findAlertKeyToShow } from '../../lib/alerts';
+
+export const getAlert = (state) => (file) => findAlertKeyToShow(state, file);
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 76ba8339703..77755b179ef 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -70,3 +70,8 @@ export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
+
+// Alert mutation types
+
+export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT';
+export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 576f861a090..48648796e66 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import * as types from './mutation_types';
+import alertMutations from './mutations/alert';
import branchMutations from './mutations/branch';
import fileMutations from './mutations/file';
import mergeRequestMutation from './mutations/merge_request';
@@ -244,4 +245,5 @@ export default {
...fileMutations,
...treeMutations,
...branchMutations,
+ ...alertMutations,
};
diff --git a/app/assets/javascripts/ide/stores/mutations/alert.js b/app/assets/javascripts/ide/stores/mutations/alert.js
new file mode 100644
index 00000000000..bb2d33a836b
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/alert.js
@@ -0,0 +1,21 @@
+import {
+ DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
+ DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
+} from '../mutation_types';
+
+export default {
+ [DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) {
+ if (!stages) {
+ return;
+ }
+ const hasEnvironments = stages?.nodes?.some((stage) =>
+ stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)),
+ );
+ const hasParsedCi = Array.isArray(stages.nodes);
+
+ state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi;
+ },
+ [DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) {
+ state.environmentsGuidanceAlertDismissed = true;
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index c1a83bf0726..83551e87f09 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -30,4 +30,6 @@ export default () => ({
renderWhitespaceInCode: false,
editorTheme: DEFAULT_THEME,
codesandboxBundlerUrl: null,
+ environmentsGuidanceAlertDismissed: false,
+ environmentsGuidanceAlertDetected: false,
});
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index f337520b0db..3daa5eebcb6 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlButton,
GlEmptyState,
GlDropdown,
GlDropdownItem,
@@ -8,10 +9,13 @@ import {
GlLoadingIcon,
GlSearchBoxByClick,
GlSprintf,
+ GlSafeHtmlDirective as SafeHtml,
+ GlTooltip,
} from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
+import { STATUSES } from '../../constants';
+import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
@@ -23,6 +27,7 @@ const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
export default {
components: {
+ GlButton,
GlEmptyState,
GlDropdown,
GlDropdownItem,
@@ -31,9 +36,13 @@ export default {
GlLoadingIcon,
GlSearchBoxByClick,
GlSprintf,
+ GlTooltip,
ImportTableRow,
PaginationLinks,
},
+ directives: {
+ SafeHtml,
+ },
props: {
sourceUrl: {
@@ -65,12 +74,28 @@ export default {
},
computed: {
+ groups() {
+ return this.bulkImportSourceGroups?.nodes ?? [];
+ },
+
+ hasGroupsWithValidationError() {
+ return this.groups.some((g) => g.validation_errors.length);
+ },
+
+ availableGroupsForImport() {
+ return this.groups.filter((g) => g.progress.status === STATUSES.NONE);
+ },
+
+ isImportAllButtonDisabled() {
+ return this.hasGroupsWithValidationError || this.availableGroupsForImport.length === 0;
+ },
+
humanizedTotal() {
return this.paginationInfo.total >= 1000 ? __('1000+') : this.paginationInfo.total;
},
hasGroups() {
- return this.bulkImportSourceGroups?.nodes?.length > 0;
+ return this.groups.length > 0;
},
hasEmptyFilter() {
@@ -105,6 +130,10 @@ export default {
},
methods: {
+ groupsCount(count) {
+ return n__('%d group', '%d groups', count);
+ },
+
setPage(page) {
this.page = page;
},
@@ -123,24 +152,57 @@ export default {
});
},
- importGroup(sourceGroupId) {
+ importGroups(sourceGroupIds) {
this.$apollo.mutate({
- mutation: importGroupMutation,
- variables: { sourceGroupId },
+ mutation: importGroupsMutation,
+ variables: { sourceGroupIds },
});
},
+ importAllGroups() {
+ this.importGroups(this.availableGroupsForImport.map((g) => g.id));
+ },
+
setPageSize(size) {
this.perPage = size;
},
},
+ gitlabLogo: window.gon.gitlab_logo,
PAGE_SIZES,
};
</script>
<template>
<div>
+ <h1
+ class="gl-my-0 gl-py-4 gl-font-size-h1 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
+ >
+ <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
+ {{ s__('BulkImport|Import groups from GitLab') }}
+ <div ref="importAllButtonWrapper" class="gl-ml-auto">
+ <gl-button
+ v-if="!$apollo.loading && hasGroups"
+ :disabled="isImportAllButtonDisabled"
+ variant="confirm"
+ @click="importAllGroups"
+ >
+ <gl-sprintf :message="s__('BulkImport|Import %{groups}')">
+ <template #groups>
+ {{ groupsCount(availableGroupsForImport.length) }}
+ </template>
+ </gl-sprintf>
+ </gl-button>
+ </div>
+ <gl-tooltip v-if="isImportAllButtonDisabled" :target="() => $refs.importAllButtonWrapper">
+ <template v-if="hasGroupsWithValidationError">
+ {{ s__('BulkImport|One or more groups has validation errors') }}
+ </template>
+ <template v-else>
+ {{ s__('BulkImport|No groups on this page are available for import') }}
+ </template>
+ </gl-tooltip>
+ </h1>
<div
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
@@ -153,7 +215,7 @@ export default {
<strong>{{ paginationInfo.end }}</strong>
</template>
<template #total>
- <strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong>
+ <strong>{{ groupsCount(paginationInfo.total) }}</strong>
</template>
<template #filter>
<strong>{{ filter }}</strong>
@@ -180,7 +242,7 @@ export default {
:description="s__('Check your source instance permissions.')"
/>
<template v-else>
- <table class="gl-w-full">
+ <table class="gl-w-full" data-qa-selector="import_table">
<thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
<th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
<th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
@@ -196,7 +258,7 @@ export default {
:group-path-regex="groupPathRegex"
@update-target-namespace="updateTargetNamespace(group.id, $event)"
@update-new-name="updateNewName(group.id, $event)"
- @import-group="importGroup(group.id)"
+ @import-group="importGroups([group.id])"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
index aed879e75fb..60cd5bb0a96 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
@@ -10,8 +10,11 @@ import {
GlFormInput,
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
+import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql';
+import removeValidationErrorMutation from '../graphql/mutations/remove_validation_error.mutation.graphql';
import groupQuery from '../graphql/queries/group.query.graphql';
const DEBOUNCE_INTERVAL = 300;
@@ -52,6 +55,27 @@ export default {
fullPath: this.fullPath,
};
},
+ update({ existingGroup }) {
+ const variables = {
+ field: 'new_name',
+ sourceGroupId: this.group.id,
+ };
+
+ if (!existingGroup) {
+ this.$apollo.mutate({
+ mutation: removeValidationErrorMutation,
+ variables,
+ });
+ } else {
+ this.$apollo.mutate({
+ mutation: addValidationErrorMutation,
+ variables: {
+ ...variables,
+ message: s__('BulkImport|Name already exists.'),
+ },
+ });
+ }
+ },
skip() {
return !this.isNameValid || this.isAlreadyImported;
},
@@ -63,8 +87,12 @@ export default {
return this.group.import_target;
},
+ invalidNameValidationMessage() {
+ return this.group.validation_errors.find(({ field }) => field === 'new_name')?.message;
+ },
+
isInvalid() {
- return Boolean(!this.isNameValid || this.existingGroup);
+ return Boolean(!this.isNameValid || this.invalidNameValidationMessage);
},
isNameValid() {
@@ -72,11 +100,11 @@ export default {
},
isAlreadyImported() {
- return this.group.status !== STATUSES.NONE;
+ return this.group.progress.status !== STATUSES.NONE;
},
isFinished() {
- return this.group.status === STATUSES.FINISHED;
+ return this.group.progress.status === STATUSES.FINISHED;
},
fullPath() {
@@ -91,7 +119,11 @@ export default {
</script>
<template>
- <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid">
+ <tr
+ class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"
+ data-qa-selector="import_item"
+ :data-qa-source-group="group.full_path"
+ >
<td class="gl-p-4">
<gl-link
:href="group.web_url"
@@ -122,6 +154,7 @@ export default {
:disabled="isAlreadyImported"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1"
+ data-qa-selector="target_namespace_selector_dropdown"
>
<gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
s__('BulkImport|No parent')
@@ -134,6 +167,8 @@ export default {
<gl-dropdown-item
v-for="ns in availableNamespaces"
:key="ns.full_path"
+ data-qa-selector="target_group_dropdown_item"
+ :data-qa-group-name="ns.full_path"
@click="$emit('update-target-namespace', ns.full_path)"
>
{{ ns.full_path }}
@@ -157,22 +192,23 @@ export default {
<template v-if="!isNameValid">
{{ __('Please choose a group URL with no special characters.') }}
</template>
- <template v-else-if="existingGroup">
- {{ s__('BulkImport|Name already exists.') }}
+ <template v-else-if="invalidNameValidationMessage">
+ {{ invalidNameValidationMessage }}
</template>
</p>
</div>
</div>
</td>
- <td class="gl-p-4 gl-white-space-nowrap">
- <import-status :status="group.status" />
+ <td class="gl-p-4 gl-white-space-nowrap" data-qa-selector="import_status_indicator">
+ <import-status :status="group.progress.status" class="gl-mt-2" />
</td>
<td class="gl-p-4">
<gl-button
v-if="!isAlreadyImported"
:disabled="isInvalid"
- variant="success"
+ variant="confirm"
category="secondary"
+ data-qa-selector="import_group_button"
@click="$emit('import-group')"
>{{ __('Import') }}</gl-button
>
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index d444cc77aa7..2cde3781a6a 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -4,40 +4,83 @@ import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
+import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
+import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql';
+import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
+import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql';
import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
+import typeDefs from './typedefs.graphql';
export const clientTypenames = {
BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
AvailableNamespace: 'ClientAvailableNamespace',
BulkImportPageInfo: 'ClientBulkImportPageInfo',
+ BulkImportTarget: 'ClientBulkImportTarget',
+ BulkImportProgress: 'ClientBulkImportProgress',
+ BulkImportValidationError: 'ClientBulkImportValidationError',
};
-export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
- let statusPoller;
+function makeGroup(data) {
+ const result = {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ ...data,
+ };
+ const NESTED_OBJECT_FIELDS = {
+ import_target: clientTypenames.BulkImportTarget,
+ progress: clientTypenames.BulkImportProgress,
+ };
- let sourceGroupManager;
- const getGroupsManager = (client) => {
- if (!sourceGroupManager) {
- sourceGroupManager = new GroupsManager({ client, sourceUrl });
+ Object.entries(NESTED_OBJECT_FIELDS).forEach(([field, type]) => {
+ if (!data[field]) {
+ return;
}
- return sourceGroupManager;
- };
+ result[field] = {
+ __typename: type,
+ ...data[field],
+ };
+ });
+
+ return result;
+}
+
+const localProgressId = (id) => `not-started-${id}`;
+
+export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
+ const groupsManager = new GroupsManager({
+ sourceUrl,
+ });
+
+ let statusPoller;
return {
Query: {
+ async bulkImportSourceGroup(_, { id }, { client, getCacheKey }) {
+ return client.readFragment({
+ fragment: bulkImportSourceGroupItemFragment,
+ fragmentName: 'BulkImportSourceGroupItem',
+ id: getCacheKey({
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id,
+ }),
+ });
+ },
+
async bulkImportSourceGroups(_, vars, { client }) {
if (!statusPoller) {
statusPoller = new StatusPoller({
- groupManager: getGroupsManager(client),
+ updateImportStatus: ({ id, status_name: status }) =>
+ client.mutate({
+ mutation: updateImportStatusMutation,
+ variables: { id, status },
+ }),
pollPath: endpoints.jobs,
});
statusPoller.startPolling();
}
- const groupsManager = getGroupsManager(client);
return Promise.all([
axios.get(endpoints.status, {
params: {
@@ -59,19 +102,21 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return {
__typename: clientTypenames.BulkImportSourceGroupConnection,
nodes: data.importable_data.map((group) => {
- const cachedImportState = groupsManager.getImportStateFromStorageByGroupId(
- group.id,
- );
+ const { jobId, importState: cachedImportState } =
+ groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {};
- return {
- __typename: clientTypenames.BulkImportSourceGroup,
+ return makeGroup({
...group,
- status: cachedImportState?.status ?? STATUSES.NONE,
+ validation_errors: [],
+ progress: {
+ id: jobId ?? localProgressId(group.id),
+ status: cachedImportState?.status ?? STATUSES.NONE,
+ },
import_target: cachedImportState?.importTarget ?? {
new_name: group.full_path,
target_namespace: availableNamespaces[0]?.full_path ?? '',
},
- };
+ });
}),
pageInfo: {
__typename: clientTypenames.BulkImportPageInfo,
@@ -91,46 +136,149 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
),
},
Mutation: {
- setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) {
- getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => {
- // eslint-disable-next-line no-param-reassign
- sourceGroup.import_target.target_namespace = targetNamespace;
+ setTargetNamespace: (_, { targetNamespace, sourceGroupId }) =>
+ makeGroup({
+ id: sourceGroupId,
+ import_target: {
+ target_namespace: targetNamespace,
+ },
+ }),
+
+ setNewName: (_, { newName, sourceGroupId }) =>
+ makeGroup({
+ id: sourceGroupId,
+ import_target: {
+ new_name: newName,
+ },
+ }),
+
+ async setImportProgress(_, { sourceGroupId, status, jobId }) {
+ if (jobId) {
+ groupsManager.updateImportProgress(jobId, status);
+ }
+
+ return makeGroup({
+ id: sourceGroupId,
+ progress: {
+ id: jobId ?? localProgressId(sourceGroupId),
+ status,
+ },
});
},
- setNewName(_, { newName, sourceGroupId }, { client }) {
- getGroupsManager(client).updateById(sourceGroupId, (sourceGroup) => {
- // eslint-disable-next-line no-param-reassign
- sourceGroup.import_target.new_name = newName;
+ async updateImportStatus(_, { id, status }) {
+ groupsManager.updateImportProgress(id, status);
+
+ return {
+ __typename: clientTypenames.BulkImportProgress,
+ id,
+ status,
+ };
+ },
+
+ async addValidationError(_, { sourceGroupId, field, message }, { client }) {
+ const {
+ data: {
+ bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
+ },
+ } = await client.query({
+ query: bulkImportSourceGroupQuery,
+ variables: { id: sourceGroupId },
});
+
+ return {
+ ...group,
+ validation_errors: [
+ ...validationErrors.filter(({ field: f }) => f !== field),
+ {
+ __typename: clientTypenames.BulkImportValidationError,
+ field,
+ message,
+ },
+ ],
+ };
},
- async importGroup(_, { sourceGroupId }, { client }) {
- const groupManager = getGroupsManager(client);
- const group = groupManager.findById(sourceGroupId);
- groupManager.setImportStatus(group, STATUSES.SCHEDULING);
- try {
- const response = await axios.post(endpoints.createBulkImport, {
- bulk_import: [
- {
- source_type: 'group_entity',
- source_full_path: group.full_path,
- destination_namespace: group.import_target.target_namespace,
- destination_name: group.import_target.new_name,
- },
- ],
- });
- groupManager.startImport({ group, importId: response.data.id });
- } catch (e) {
- const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed');
- createFlash({ message });
- groupManager.setImportStatus(group, STATUSES.NONE);
- throw e;
- }
+ async removeValidationError(_, { sourceGroupId, field }, { client }) {
+ const {
+ data: {
+ bulkImportSourceGroup: { validation_errors: validationErrors, ...group },
+ },
+ } = await client.query({
+ query: bulkImportSourceGroupQuery,
+ variables: { id: sourceGroupId },
+ });
+
+ return {
+ ...group,
+ validation_errors: validationErrors.filter(({ field: f }) => f !== field),
+ };
+ },
+
+ async importGroups(_, { sourceGroupIds }, { client }) {
+ const groups = await Promise.all(
+ sourceGroupIds.map((id) =>
+ client
+ .query({
+ query: bulkImportSourceGroupQuery,
+ variables: { id },
+ })
+ .then(({ data }) => data.bulkImportSourceGroup),
+ ),
+ );
+
+ const GROUPS_BEING_SCHEDULED = sourceGroupIds.map((sourceGroupId) =>
+ makeGroup({
+ id: sourceGroupId,
+ progress: {
+ id: localProgressId(sourceGroupId),
+ status: STATUSES.SCHEDULING,
+ },
+ }),
+ );
+
+ const defaultErrorMessage = s__('BulkImport|Importing the group failed');
+ axios
+ .post(endpoints.createBulkImport, {
+ bulk_import: groups.map((group) => ({
+ source_type: 'group_entity',
+ source_full_path: group.full_path,
+ destination_namespace: group.import_target.target_namespace,
+ destination_name: group.import_target.new_name,
+ })),
+ })
+ .then(({ data: { id: jobId } }) => {
+ groupsManager.createImportState(jobId, {
+ status: STATUSES.CREATED,
+ groups,
+ });
+
+ return { status: STATUSES.CREATED, jobId };
+ })
+ .catch((e) => {
+ const message = e?.response?.data?.error ?? defaultErrorMessage;
+ createFlash({ message });
+ return { status: STATUSES.NONE };
+ })
+ .then((newStatus) =>
+ sourceGroupIds.forEach((sourceGroupId) =>
+ client.mutate({
+ mutation: setImportProgressMutation,
+ variables: { sourceGroupId, ...newStatus },
+ }),
+ ),
+ )
+ .catch(() => createFlash({ message: defaultErrorMessage }));
+
+ return GROUPS_BEING_SCHEDULED;
},
},
};
}
export const createApolloClient = ({ sourceUrl, endpoints }) =>
- createDefaultClient(createResolvers({ sourceUrl, endpoints }), { assumeImmutableResults: true });
+ createDefaultClient(
+ createResolvers({ sourceUrl, endpoints }),
+ { assumeImmutableResults: true },
+ typeDefs,
+ );
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
index 50774e36599..47675cd1bd0 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -1,8 +1,19 @@
+#import "./bulk_import_source_group_progress.fragment.graphql"
+
fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
id
web_url
full_path
full_name
- status
- import_target
+ progress {
+ ...BulkImportSourceGroupProgress
+ }
+ import_target {
+ target_namespace
+ new_name
+ }
+ validation_errors {
+ field
+ message
+ }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
new file mode 100644
index 00000000000..2d60bf82d65
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
@@ -0,0 +1,4 @@
+fragment BulkImportSourceGroupProgress on ClientBulkImportProgress {
+ id
+ status
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
new file mode 100644
index 00000000000..d95c460c046
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
@@ -0,0 +1,9 @@
+mutation addValidationError($sourceGroupId: String!, $field: String!, $message: String!) {
+ addValidationError(sourceGroupId: $sourceGroupId, field: $field, message: $message) @client {
+ id
+ validation_errors {
+ field
+ message
+ }
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
deleted file mode 100644
index 412608d3faf..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation importGroup($sourceGroupId: String!) {
- importGroup(sourceGroupId: $sourceGroupId) @client
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
new file mode 100644
index 00000000000..d8e46329e38
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
@@ -0,0 +1,9 @@
+mutation importGroups($sourceGroupIds: [String!]!) {
+ importGroups(sourceGroupIds: $sourceGroupIds) @client {
+ id
+ progress {
+ id
+ status
+ }
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
new file mode 100644
index 00000000000..940bf4dfaac
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
@@ -0,0 +1,9 @@
+mutation removeValidationError($sourceGroupId: String!, $field: String!) {
+ removeValidationError(sourceGroupId: $sourceGroupId, field: $field) @client {
+ id
+ validation_errors {
+ field
+ message
+ }
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
new file mode 100644
index 00000000000..2ec1269932a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
@@ -0,0 +1,9 @@
+mutation setImportProgress($status: String!, $sourceGroupId: String!, $jobId: String) {
+ setImportProgress(status: $status, sourceGroupId: $sourceGroupId, jobId: $jobId) @client {
+ id
+ progress {
+ id
+ status
+ }
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
index 2bc19891401..354bf2a5815 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
@@ -1,3 +1,8 @@
mutation setNewName($newName: String!, $sourceGroupId: String!) {
- setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client
+ setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client {
+ id
+ import_target {
+ new_name
+ }
+ }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
index fc98a1652c1..a0ef407f135 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
@@ -1,3 +1,8 @@
mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
- setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client
+ setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client {
+ id
+ import_target {
+ target_namespace
+ }
+ }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql
new file mode 100644
index 00000000000..8c0233b2939
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql
@@ -0,0 +1,6 @@
+mutation updateImportStatus($status: String!, $id: String!) {
+ updateImportStatus(status: $status, id: $id) @client {
+ id
+ status
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
new file mode 100644
index 00000000000..0aff23af96d
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_group.query.graphql
@@ -0,0 +1,7 @@
+#import "../fragments/bulk_import_source_group_item.fragment.graphql"
+
+query bulkImportSourceGroup($id: ID!) {
+ bulkImportSourceGroup(id: $id) @client {
+ ...BulkImportSourceGroupItem
+ }
+}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
index 2c88d25358f..97dbdbf518a 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -1,26 +1,10 @@
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
-import produce from 'immer';
import { debounce, merge } from 'lodash';
-import { STATUSES } from '../../../constants';
-import ImportSourceGroupFragment from '../fragments/bulk_import_source_group_item.fragment.graphql';
-
-function extractTypeConditionFromFragment(fragment) {
- return fragment.definitions[0]?.typeCondition.name.value;
-}
-
-function generateGroupId(id) {
- return defaultDataIdFromObject({
- __typename: extractTypeConditionFromFragment(ImportSourceGroupFragment),
- id,
- });
-}
export const KEY = 'gl-bulk-imports-import-state';
export const DEBOUNCE_INTERVAL = 200;
export class SourceGroupsManager {
- constructor({ client, sourceUrl, storage = window.localStorage }) {
- this.client = client;
+ constructor({ sourceUrl, storage = window.localStorage }) {
this.sourceUrl = sourceUrl;
this.storage = storage;
@@ -29,51 +13,58 @@ export class SourceGroupsManager {
loadImportStatesFromStorage() {
try {
- return JSON.parse(this.storage.getItem(KEY)) ?? {};
+ return Object.fromEntries(
+ Object.entries(JSON.parse(this.storage.getItem(KEY)) ?? {}).map(([jobId, config]) => {
+ // new format of storage
+ if (config.groups) {
+ return [jobId, config];
+ }
+
+ return [
+ jobId,
+ {
+ status: config.status,
+ groups: [{ id: config.id, importTarget: config.importTarget }],
+ },
+ ];
+ }),
+ );
} catch {
return {};
}
}
- findById(id) {
- const cacheId = generateGroupId(id);
- return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId });
- }
-
- update(group, fn) {
- this.client.writeFragment({
- fragment: ImportSourceGroupFragment,
- id: generateGroupId(group.id),
- data: produce(group, fn),
- });
- }
-
- updateById(id, fn) {
- const group = this.findById(id);
- this.update(group, fn);
- }
-
- saveImportState(importId, group) {
+ createImportState(importId, jobConfig) {
this.importStates[this.getStorageKey(importId)] = {
- id: group.id,
- importTarget: group.import_target,
- status: group.status,
+ status: jobConfig.status,
+ groups: jobConfig.groups.map((g) => ({ importTarget: g.import_target, id: g.id })),
};
this.saveImportStatesToStorage();
}
- getImportStateFromStorage(importId) {
- return this.importStates[this.getStorageKey(importId)];
+ updateImportProgress(importId, status) {
+ const currentState = this.importStates[this.getStorageKey(importId)];
+ if (!currentState) {
+ return;
+ }
+
+ currentState.status = status;
+ this.saveImportStatesToStorage();
}
getImportStateFromStorageByGroupId(groupId) {
const PREFIX = this.getStorageKey('');
- const [, importState] =
+ const [jobId, importState] =
Object.entries(this.importStates).find(
- ([key, group]) => key.startsWith(PREFIX) && group.id === groupId,
+ ([key, state]) => key.startsWith(PREFIX) && state.groups.some((g) => g.id === groupId),
) ?? [];
- return importState;
+ if (!jobId) {
+ return null;
+ }
+
+ const group = importState.groups.find((g) => g.id === groupId);
+ return { jobId, importState: { ...group, status: importState.status } };
}
getStorageKey(importId) {
@@ -91,34 +82,4 @@ export class SourceGroupsManager {
// empty catch intentional: storage might be unavailable or full
}
}, DEBOUNCE_INTERVAL);
-
- startImport({ group, importId }) {
- this.setImportStatus(group, STATUSES.CREATED);
- this.saveImportState(importId, group);
- }
-
- setImportStatus(group, status) {
- this.update(group, (sourceGroup) => {
- // eslint-disable-next-line no-param-reassign
- sourceGroup.status = status;
- });
- }
-
- setImportStatusByImportId(importId, status) {
- const importState = this.getImportStateFromStorage(importId);
- if (!importState) {
- return;
- }
-
- if (importState.status !== status) {
- importState.status = status;
- }
-
- const group = this.findById(importState.id);
- if (group?.id) {
- this.setImportStatus(group, status);
- }
-
- this.saveImportStatesToStorage();
- }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
index b80a575afce..0297b3d3428 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
@@ -5,13 +5,15 @@ import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
export class StatusPoller {
- constructor({ groupManager, pollPath }) {
+ constructor({ updateImportStatus, pollPath }) {
this.eTagPoll = new Poll({
resource: {
fetchJobs: () => axios.get(pollPath),
},
method: 'fetchJobs',
- successCallback: ({ data }) => this.updateImportsStatuses(data),
+ successCallback: ({ data: statuses }) => {
+ statuses.forEach((status) => updateImportStatus(status));
+ },
errorCallback: () =>
createFlash({
message: s__('BulkImport|Update of import statuses with realtime changes failed'),
@@ -25,17 +27,9 @@ export class StatusPoller {
this.eTagPoll.stop();
}
});
-
- this.groupManager = groupManager;
}
startPolling() {
this.eTagPoll.makeRequest();
}
-
- async updateImportsStatuses(importStatuses) {
- importStatuses.forEach(({ id, status_name: statusName }) => {
- this.groupManager.setImportStatusByImportId(id, statusName);
- });
- }
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
new file mode 100644
index 00000000000..c830aaa75e6
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -0,0 +1,65 @@
+type ClientBulkImportAvailableNamespace {
+ id: ID!
+ full_path: String!
+}
+
+type ClientBulkImportTarget {
+ target_namespace: String!
+ new_name: String!
+}
+
+type ClientBulkImportSourceGroupConnection {
+ nodes: [ClientBulkImportSourceGroup!]!
+ pageInfo: ClientBulkImportPageInfo!
+}
+
+type ClientBulkImportProgress {
+ id: ID
+ status: String!
+}
+
+type ClientBulkImportValidationError {
+ field: String!
+ message: String!
+}
+
+type ClientBulkImportSourceGroup {
+ id: ID!
+ web_url: String!
+ full_path: String!
+ full_name: String!
+ progress: ClientBulkImportProgress!
+ import_target: ClientBulkImportTarget!
+ validation_errors: [ClientBulkImportValidationError!]!
+}
+
+type ClientBulkImportPageInfo {
+ page: Int!
+ perPage: Int!
+ total: Int!
+ totalPages: Int!
+}
+
+extend type Query {
+ bulkImportSourceGroup(id: ID!): ClientBulkImportSourceGroup
+ bulkImportSourceGroups(
+ page: Int!
+ perPage: Int!
+ filter: String!
+ ): ClientBulkImportSourceGroupConnection!
+ availableNamespaces: [ClientBulkImportAvailableNamespace!]!
+}
+
+extend type Mutation {
+ setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
+ setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
+ importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]!
+ setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup!
+ updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
+ addValidationError(
+ sourceGroupId: ID!
+ field: String!
+ message: String!
+ ): ClientBulkImportSourceGroup!
+ removeValidationError(sourceGroupId: ID!, field: String!): ClientBulkImportSourceGroup!
+}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 5638dc064d1..af99341b11f 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -10,7 +10,6 @@ import {
GlIcon,
GlEmptyState,
} from '@gitlab/ui';
-import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
@@ -49,6 +48,7 @@ export default {
label: s__('IncidentManagement|Severity'),
thClass: `${thClass} w-15p`,
tdClass: `${tdClass} sortable-cell`,
+ actualSortKey: 'SEVERITY',
sortable: true,
thAttr: TH_SEVERITY_TEST_ID,
},
@@ -63,6 +63,7 @@ export default {
label: s__('IncidentManagement|Date created'),
thClass: `${thClass} gl-w-eighth`,
tdClass: `${tdClass} sortable-cell`,
+ actualSortKey: 'CREATED',
sortable: true,
thAttr: TH_CREATED_AT_TEST_ID,
},
@@ -72,7 +73,7 @@ export default {
thClass: `gl-text-right gl-w-eighth`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
- sortKey: 'SLA_DUE_AT',
+ actualSortKey: 'SLA_DUE_AT',
sortable: true,
sortDirection: 'asc',
},
@@ -87,6 +88,7 @@ export default {
label: s__('IncidentManagement|Published'),
thClass: `${thClass} w-15p`,
tdClass: `${tdClass} sortable-cell`,
+ actualSortKey: 'PUBLISHED',
sortable: true,
thAttr: TH_PUBLISHED_TEST_ID,
},
@@ -174,8 +176,7 @@ export default {
redirecting: false,
incidents: {},
incidentsCount: {},
- sort: 'created_desc',
- sortBy: 'createdAt',
+ sort: 'CREATED_DESC',
sortDesc: true,
statusFilter: '',
filteredByStatus: '',
@@ -256,20 +257,17 @@ export default {
this.redirecting = true;
},
fetchSortedData({ sortBy, sortDesc }) {
- let sortKey;
- // In bootstrap-vue v2.17.0, sortKey becomes natively supported and we can eliminate this function
const field = this.availableFields.find(({ key }) => key === sortBy);
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
- // Use `sortKey` if provided, otherwise fall back to existing algorithm
- if (field?.sortKey) {
- sortKey = field.sortKey;
- } else {
- sortKey = convertToSnakeCase(sortBy).replace(/_.*/, '').toUpperCase();
- }
-
this.pagination = initialPaginationState;
- this.sort = `${sortKey}_${sortingDirection}`;
+
+ // BootstapVue natively supports a `sortKey` parameter, but using it results in the sorting
+ // icons not being updated properly in the header. We decided to fallback on `actualSortKey`
+ // to bypass BootstrapVue's behavior until the bug is addressed upstream.
+ // Related discussion: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60926/diffs#note_568020482
+ // Upstream issue: https://github.com/bootstrap-vue/bootstrap-vue/issues/6602
+ this.sort = `${field.actualSortKey}_${sortingDirection}`;
},
getSeverity(severity) {
return INCIDENT_SEVERITY[severity];
@@ -334,14 +332,14 @@ export default {
<gl-table
:items="incidents.list || []"
:fields="availableFields"
- :show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
- :no-local-sorting="true"
- :sort-direction="'desc'"
+ sort-direction="desc"
:sort-desc.sync="sortDesc"
- :sort-by.sync="sortBy"
+ sort-by="createdAt"
+ show-empty
+ no-local-sorting
sort-icon-left
fixed
@row-clicked="navigateToIncidentDetails"
diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js
index 577d8ecb777..d479838b491 100644
--- a/app/assets/javascripts/incidents_settings/constants.js
+++ b/app/assets/javascripts/incidents_settings/constants.js
@@ -43,7 +43,9 @@ export const I18N_ALERT_SETTINGS_FORM = {
label: __('Send a single email notification to Owners and Maintainers for new alerts.'),
},
autoCloseIncidents: {
- label: __('Automatically close incidents when the associated Prometheus alert resolves.'),
+ label: __(
+ 'Automatically close associated incident when a recovery alert notification resolves an alert',
+ ),
},
};
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 59038b3d9fb..17c73fdf1c3 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,7 +1,6 @@
/* eslint-disable no-new */
import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
-import DueDateSelectors from './due_date_select';
import IssuableContext from './issuable_context';
import LabelsSelect from './labels_select';
import MilestoneSelect from './milestone_select';
@@ -19,7 +18,6 @@ export default () => {
});
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
- new DueDateSelectors();
Sidebar.initialize();
mountSidebarLabels();
diff --git a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
index 1e33ceb7835..f7d7f4aa010 100644
--- a/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue
@@ -37,7 +37,6 @@ export default {
<input name="service[active]" type="hidden" :value="activated || false" />
<gl-form-checkbox
v-model="activated"
- name="service[active]"
class="gl-display-block"
:disabled="isInheriting"
@change="onChange"
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index aea4a8b1c0b..9bc01cdd9fc 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -1,6 +1,5 @@
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
@@ -16,7 +15,6 @@ export default {
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
- mixins: [glFeatureFlagsMixin()],
props: {
showJiraIssuesIntegration: {
type: Boolean,
@@ -76,7 +74,7 @@ export default {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
showJiraVulnerabilitiesOptions() {
- return this.showJiraVulnerabilitiesIntegration && this.glFeatures.jiraForVulnerabilities;
+ return this.showJiraVulnerabilitiesIntegration;
},
showUltimateUpgrade() {
return this.showJiraIssuesIntegration && !this.showJiraVulnerabilitiesIntegration;
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index b0f19e5b585..93d8bcc4c19 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -16,13 +16,13 @@ const commentDetailOptions = [
{
value: 'standard',
label: s__('Integrations|Standard'),
- help: s__('Integrations|Includes commit title and branch'),
+ help: s__('Integrations|Includes commit title and branch.'),
},
{
value: 'all_details',
label: s__('Integrations|All details'),
help: s__(
- 'Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs',
+ 'Integrations|Includes Standard, plus the entire commit message, commit hash, and issue IDs',
),
},
];
@@ -144,7 +144,7 @@ export default {
label-for="service[trigger]"
:description="
s__(
- 'Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) is created.',
+ 'Integrations|When you mention a Jira issue in a commit or merge request, GitLab creates a remote link and comment (if enabled).',
)
"
>
diff --git a/app/assets/javascripts/invite_member/components/invite_member_modal.vue b/app/assets/javascripts/invite_member/components/invite_member_modal.vue
deleted file mode 100644
index ec77e49ae53..00000000000
--- a/app/assets/javascripts/invite_member/components/invite_member_modal.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<script>
-import { GlModal, GlLink } from '@gitlab/ui';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import { s__, __ } from '~/locale';
-import { OPEN_MODAL, MODAL_ID } from '../constants';
-import eventHub from '../event_hub';
-
-export default {
- cancelProps: {
- text: __('Got it'),
- attributes: [
- {
- variant: 'info',
- },
- ],
- },
- modalId: MODAL_ID,
- components: {
- GlLink,
- GlModal,
- },
- props: {
- membersPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- i18n: {
- modalTitle: s__("InviteMember|Oops, this feature isn't ready yet"),
- bodyTopMessage: s__(
- "InviteMember|We're working to allow everyone to invite new members, making it easier for teams to get started with GitLab",
- ),
- bodyMiddleMessage: s__(
- 'InviteMember|Until then, ask an owner to invite new project members for you',
- ),
- linkText: s__('InviteMember|See who can invite members for you'),
- },
- mounted() {
- eventHub.$on(OPEN_MODAL, this.openModal);
- },
- methods: {
- openModal() {
- this.$root.$emit(BV_SHOW_MODAL, MODAL_ID);
- },
- },
-};
-</script>
-<template>
- <gl-modal :modal-id="$options.modalId" size="sm" :action-cancel="$options.cancelProps">
- <template #modal-title>
- {{ $options.i18n.modalTitle }}
- <gl-emoji
- class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
- data-name="sweat_smile"
- />
- </template>
- <p>{{ $options.i18n.bodyTopMessage }}</p>
- <p>{{ $options.i18n.bodyMiddleMessage }}</p>
- <gl-link
- :href="membersPath"
- data-track-event="click_who_can_invite_link"
- data-track-label="invite_members_message"
- >{{ $options.i18n.linkText }}</gl-link
- >
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue b/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
deleted file mode 100644
index ee89e0bbf71..00000000000
--- a/app/assets/javascripts/invite_member/components/invite_member_trigger.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlLink } from '@gitlab/ui';
-import { OPEN_MODAL } from '../constants';
-import eventHub from '../event_hub';
-
-export default {
- components: {
- GlLink,
- },
- props: {
- displayText: {
- type: String,
- required: false,
- default: '',
- },
- event: {
- type: String,
- required: false,
- default: '',
- },
- label: {
- type: String,
- required: false,
- default: '',
- },
- },
- methods: {
- openModal() {
- eventHub.$emit(OPEN_MODAL);
- },
- },
-};
-</script>
-
-<template>
- <gl-link
- data-is-link="true"
- :data-track-event="event"
- :data-track-label="label"
- @click="openModal"
- >{{ displayText }}
- </gl-link>
-</template>
diff --git a/app/assets/javascripts/invite_member/constants.js b/app/assets/javascripts/invite_member/constants.js
deleted file mode 100644
index fee6e7a260a..00000000000
--- a/app/assets/javascripts/invite_member/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const OPEN_MODAL = 'openModal';
-export const MODAL_ID = 'invite-member-modal';
diff --git a/app/assets/javascripts/invite_member/event_hub.js b/app/assets/javascripts/invite_member/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/invite_member/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js
deleted file mode 100644
index a50d31c9e7a..00000000000
--- a/app/assets/javascripts/invite_member/init_invite_member_modal.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { GlToast } from '@gitlab/ui';
-import Vue from 'vue';
-import { isInIssuePage, isInDesignPage } from '~/lib/utils/common_utils';
-import InviteMemberModal from './components/invite_member_modal.vue';
-
-Vue.use(GlToast);
-
-const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget;
-
-export default function initInviteMembersModal() {
- const el = document.querySelector('.js-invite-member-modal');
-
- if (!el || isAssigneesWidgetShown) {
- return false;
- }
-
- const { membersPath } = el.dataset;
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(InviteMemberModal, {
- props: { membersPath },
- }),
- });
-}
diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js
deleted file mode 100644
index eb765ae83b0..00000000000
--- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import InviteMemberTrigger from './components/invite_member_trigger.vue';
-
-export default function initInviteMembersTrigger() {
- const el = document.querySelector('.js-invite-member-trigger');
-
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(InviteMemberTrigger, {
- props: { ...el.dataset },
- }),
- });
-}
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index 7bdd55ddda3..f17440a4a14 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -21,13 +21,11 @@ export default {
props: {
exportCsvPath: {
type: String,
- required: false,
- default: '',
+ required: true,
},
issuableCount: {
type: Number,
- required: false,
- default: 0,
+ required: true,
},
modalId: {
type: String,
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
new file mode 100644
index 00000000000..cb768f2bc5b
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/status_box.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import { fetchPolicies } from '~/lib/graphql';
+import { __ } from '~/locale';
+
+export const statusBoxState = Vue.observable({
+ state: '',
+ updateStatus: null,
+});
+
+const CLASSES = {
+ opened: 'status-box-open',
+ locked: 'status-box-open',
+ closed: 'status-box-mr-closed',
+ merged: 'status-box-mr-merged',
+};
+
+const STATUS = {
+ opened: [__('Open'), 'issue-open-m'],
+ locked: [__('Open'), 'issue-open-m'],
+ closed: [__('Closed'), 'issue-close'],
+ merged: [__('Merged'), 'git-merge'],
+};
+
+export default {
+ components: {
+ GlIcon,
+ },
+ inject: {
+ query: { default: null },
+ projectPath: { default: null },
+ iid: { default: null },
+ },
+ props: {
+ initialState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ if (this.initialState) {
+ statusBoxState.state = this.initialState;
+ }
+
+ return statusBoxState;
+ },
+ computed: {
+ statusBoxClass() {
+ return CLASSES[`${this.issuableType}_${this.state}`] || CLASSES[this.state];
+ },
+ statusHumanName() {
+ return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[0];
+ },
+ statusIconName() {
+ return (STATUS[`${this.issuableType}_${this.state}`] || STATUS[this.state])[1];
+ },
+ },
+ created() {
+ if (!statusBoxState.updateStatus) {
+ statusBoxState.updateStatus = this.fetchState;
+ }
+ },
+ beforeDestroy() {
+ if (statusBoxState.updateStatus && this.query) {
+ statusBoxState.updateStatus = null;
+ }
+ },
+ methods: {
+ async fetchState() {
+ const { data } = await this.$apollo.query({
+ query: this.query,
+ variables: {
+ projectPath: this.projectPath,
+ iid: this.iid,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ statusBoxState.state = data?.workspace?.issuable?.state;
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="statusBoxClass" class="issuable-status-box status-box">
+ <gl-icon
+ :name="statusIconName"
+ class="gl-display-block gl-sm-display-none!"
+ data-testid="status-icon"
+ />
+ <span class="gl-display-none gl-sm-display-block">
+ {{ statusHumanName }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 153123a005f..9a1ab23e366 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -54,9 +54,9 @@ export default class IssuableForm {
this.wipRegex = new RegExp(
'^\\s*(' + // Line start, then any amount of leading whitespace
'draft\\s-\\s' + // Draft_-_ where "_" are *exactly* one whitespace
- '|\\[(draft|wip)\\]\\s*' + // [Draft] or [WIP] and any following whitespace
- '|(draft|wip):\\s*' + // Draft: or WIP: and any following whitespace
- '|(draft|wip)\\s+' + // Draft_ or WIP_ where "_" is at least one whitespace
+ '|\\[draft\\]\\s*' + // [Draft] or [WIP] and any following whitespace
+ '|draft:\\s*' + // Draft: or WIP: and any following whitespace
+ '|draft\\s+' + // Draft_ or WIP_ where "_" is at least one whitespace
'|\\(draft\\)\\s*' + // (Draft) and any following whitespace
')+' + // At least one repeated match of the preceding parenthetical
'\\s*', // Any amount of trailing whitespace
@@ -146,18 +146,12 @@ export default class IssuableForm {
workInProgress() {
return this.wipRegex.test(this.titleField.val());
}
- titlePrefixContainsDraft() {
- const prefix = this.titleField.val().match(this.wipRegex);
-
- return prefix && prefix[0].match(/draft/i);
- }
renderWipExplanation() {
if (this.workInProgress()) {
// These strings are not "translatable" (the code is hard-coded to look for them)
- this.$wipExplanation.find('code')[0].textContent = this.titlePrefixContainsDraft()
- ? 'Draft' /* eslint-disable-line @gitlab/require-i18n-strings */
- : 'WIP';
+ this.$wipExplanation.find('code')[0].textContent =
+ 'Draft'; /* eslint-disable-line @gitlab/require-i18n-strings */
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
}
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 5d497369f5a..7635536c54f 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -3,7 +3,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { getTimeago } from '~/lib/utils/datetime_utility';
+import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
@@ -50,6 +50,10 @@ export default {
},
},
computed: {
+ createdInPastDay() {
+ const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
+ return createdSecondsAgo < SECONDS_IN_DAY;
+ },
author() {
return this.issuable.author;
},
@@ -152,7 +156,12 @@ export default {
</script>
<template>
- <li :id="`issuable_${issuable.id}`" class="issue gl-px-5!" :data-labels="labelIdsString">
+ <li
+ :id="`issuable_${issuable.id}`"
+ class="issue gl-px-5!"
+ :class="{ closed: issuable.closedAt, today: createdInPastDay }"
+ :data-labels="labelIdsString"
+ >
<div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
@@ -160,7 +169,9 @@ export default {
:checked="checked"
:data-id="issuable.id"
@input="$emit('checked-input', $event)"
- />
+ >
+ <span class="gl-sr-only">{{ issuable.title }}</span>
+ </gl-form-checkbox>
</div>
<div class="issuable-main-info">
<div data-testid="issuable-title" class="issue-title title">
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index 6b95c3a578e..45584205be0 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -274,44 +274,47 @@ export default {
<gl-skeleton-loading />
</li>
</ul>
- <component
- :is="issuablesWrapper"
- v-if="!issuablesLoading && issuables.length"
- class="content-list issuable-list issues-list"
- :class="{ 'manual-ordering': isManualOrdering }"
- v-bind="$options.vueDraggableAttributes"
- @update="handleVueDraggableUpdate"
- >
- <issuable-item
- v-for="issuable in issuables"
- :key="issuableId(issuable)"
- :class="{ 'gl-cursor-grab': isManualOrdering }"
- :issuable-symbol="issuableSymbol"
- :issuable="issuable"
- :enable-label-permalinks="enableLabelPermalinks"
- :label-filter-param="labelFilterParam"
- :show-checkbox="showBulkEditSidebar"
- :checked="issuableChecked(issuable)"
- @checked-input="handleIssuableCheckedInput(issuable, $event)"
+ <template v-else>
+ <component
+ :is="issuablesWrapper"
+ v-if="issuables.length > 0"
+ class="content-list issuable-list issues-list"
+ :class="{ 'manual-ordering': isManualOrdering }"
+ v-bind="$options.vueDraggableAttributes"
+ @update="handleVueDraggableUpdate"
>
- <template #reference>
- <slot name="reference" :issuable="issuable"></slot>
- </template>
- <template #author>
- <slot name="author" :author="issuable.author"></slot>
- </template>
- <template #timeframe>
- <slot name="timeframe" :issuable="issuable"></slot>
- </template>
- <template #status>
- <slot name="status" :issuable="issuable"></slot>
- </template>
- <template #statistics>
- <slot name="statistics" :issuable="issuable"></slot>
- </template>
- </issuable-item>
- </component>
- <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot>
+ <issuable-item
+ v-for="issuable in issuables"
+ :key="issuableId(issuable)"
+ :class="{ 'gl-cursor-grab': isManualOrdering }"
+ :issuable-symbol="issuableSymbol"
+ :issuable="issuable"
+ :enable-label-permalinks="enableLabelPermalinks"
+ :label-filter-param="labelFilterParam"
+ :show-checkbox="showBulkEditSidebar"
+ :checked="issuableChecked(issuable)"
+ @checked-input="handleIssuableCheckedInput(issuable, $event)"
+ >
+ <template #reference>
+ <slot name="reference" :issuable="issuable"></slot>
+ </template>
+ <template #author>
+ <slot name="author" :author="issuable.author"></slot>
+ </template>
+ <template #timeframe>
+ <slot name="timeframe" :issuable="issuable"></slot>
+ </template>
+ <template #status>
+ <slot name="status" :issuable="issuable"></slot>
+ </template>
+ <template #statistics>
+ <slot name="statistics" :issuable="issuable"></slot>
+ </template>
+ </issuable-item>
+ </component>
+ <slot v-else name="empty-state"></slot>
+ </template>
+
<gl-pagination
v-if="showPaginationControls"
:per-page="defaultPageSize"
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
index 6bc621b52e6..dfe158ae2b0 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -48,12 +48,13 @@ export default {
<template #title>
<span :title="tab.titleTooltip">{{ tab.title }}</span>
<gl-badge
- v-if="isTabCountNumeric(tab)"
+ v-if="tabCounts && isTabCountNumeric(tab)"
variant="neutral"
size="sm"
class="gl-tab-counter-badge"
- >{{ tabCounts[tab.name] }}</gl-badge
>
+ {{ tabCounts[tab.name] }}
+ </gl-badge>
</template>
</gl-tab>
</gl-tabs>
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index d153ff21a35..01b4e81a11a 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -418,6 +418,7 @@ export default {
<div v-if="canUpdate && showForm">
<form-component
:form-state="formState"
+ :initial-description-text="initialDescriptionText"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 570bc7df3cf..14df87e486b 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -53,6 +53,7 @@ export default {
</script>
<template>
+ <!-- eslint-disable @gitlab/vue-no-data-toggle -->
<div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues">
<button
ref="toggle"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 76ea489fb86..b37a911a669 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -1,4 +1,5 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import $ from 'jquery';
import Autosave from '~/autosave';
import eventHub from '../event_hub';
@@ -15,6 +16,7 @@ export default {
descriptionField,
descriptionTemplate,
editActions,
+ GlAlert,
},
props: {
canDestroy: {
@@ -69,6 +71,16 @@ export default {
required: false,
default: true,
},
+ initialDescriptionText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ showOutdatedDescriptionWarning: false,
+ };
},
computed: {
hasIssuableTemplates() {
@@ -102,11 +114,17 @@ export default {
},
} = this.$refs;
- this.autosaveDescription = new Autosave($(textarea), [
- document.location.pathname,
- document.location.search,
- 'description',
- ]);
+ this.autosaveDescription = new Autosave(
+ $(textarea),
+ [document.location.pathname, document.location.search, 'description'],
+ null,
+ this.formState.lock_version,
+ );
+
+ const savedLockVersion = this.autosaveDescription.getSavedLockVersion();
+
+ this.showOutdatedDescriptionWarning =
+ savedLockVersion && String(this.formState.lock_version) !== savedLockVersion;
this.autosaveTitle = new Autosave($(input), [
document.location.pathname,
@@ -118,6 +136,27 @@ export default {
this.autosaveDescription.reset();
this.autosaveTitle.reset();
},
+ keepAutosave() {
+ const {
+ description: {
+ $refs: { textarea },
+ },
+ } = this.$refs;
+
+ textarea.focus();
+ this.showOutdatedDescriptionWarning = false;
+ },
+ discardAutosave() {
+ const {
+ description: {
+ $refs: { textarea },
+ },
+ } = this.$refs;
+
+ textarea.value = this.initialDescriptionText;
+ textarea.focus();
+ this.showOutdatedDescriptionWarning = false;
+ },
},
};
</script>
@@ -125,6 +164,21 @@ export default {
<template>
<form>
<locked-warning v-if="showLockedWarning" />
+ <gl-alert
+ v-if="showOutdatedDescriptionWarning"
+ class="gl-mb-5"
+ variant="warning"
+ :primary-button-text="__('Keep')"
+ :secondary-button-text="__('Discard')"
+ :dismissible="false"
+ @primaryAction="keepAutosave"
+ @secondaryAction="discardAutosave"
+ >{{
+ __(
+ 'The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?',
+ )
+ }}</gl-alert
+ >
<div class="row">
<div v-if="hasIssuableTemplates" class="col-sm-4 col-lg-3">
<description-template
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index 57c5107fcbb..93ba338a6b3 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -1,56 +1,72 @@
<script>
-import { GlButton, GlEmptyState, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlEmptyState,
+ GlFilteredSearchToken,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { toNumber } from 'lodash';
import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
+ API_PARAM,
+ apiSortParams,
CREATED_DESC,
+ i18n,
+ MAX_LIST_SIZE,
PAGE_SIZE,
- RELATIVE_POSITION_ASC,
- sortOptions,
- sortParams,
+ PARAM_DUE_DATE,
+ PARAM_PAGE,
+ PARAM_SORT,
+ PARAM_STATE,
+ RELATIVE_POSITION_DESC,
+ UPDATED_DESC,
+ URL_PARAM,
+ urlSortParams,
} from '~/issues_list/constants';
+import {
+ convertToParams,
+ convertToSearchQuery,
+ getDueDateValue,
+ getFilterTokens,
+ getSortKey,
+ getSortOptions,
+} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
-import { __, s__ } from '~/locale';
+import {
+ DEFAULT_NONE_ANY,
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_EPIC,
+ TOKEN_TITLE_ITERATION,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_WEIGHT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
+import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
+import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
export default {
- CREATED_DESC,
+ i18n,
IssuableListTabs,
- PAGE_SIZE,
- sortOptions,
- sortParams,
- i18n: {
- calendarLabel: __('Subscribe to calendar'),
- jiraIntegrationMessage: s__(
- 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
- ),
- jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'),
- jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'),
- newIssueLabel: __('New issue'),
- noClosedIssuesTitle: __('There are no closed issues'),
- noOpenIssuesDescription: __('To keep this project going, create a new issue'),
- noOpenIssuesTitle: __('There are no open issues'),
- noIssuesSignedInDescription: __(
- 'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.',
- ),
- noIssuesSignedInTitle: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project',
- ),
- noIssuesSignedOutButtonText: __('Register / Sign In'),
- noIssuesSignedOutDescription: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
- ),
- noIssuesSignedOutTitle: __('There are no issues to show'),
- noSearchResultsDescription: __('To widen your search, change or remove filters above'),
- noSearchResultsTitle: __('Sorry, your filter produced no results'),
- reorderError: __('An error occurred while reordering issues.'),
- rssLabel: __('Subscribe to RSS feed'),
- },
components: {
CsvImportExportButtons,
GlButton,
@@ -58,6 +74,7 @@ export default {
GlIcon,
GlLink,
GlSprintf,
+ IssuableByEmail,
IssuableList,
IssueCardTimeInfo,
BlockingIssuesCount: () => import('ee_component/issues/components/blocking_issues_count.vue'),
@@ -66,6 +83,12 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: {
+ autocompleteAwardEmojisPath: {
+ default: '',
+ },
+ autocompleteUsersPath: {
+ default: '',
+ },
calendarPath: {
default: '',
},
@@ -81,12 +104,24 @@ export default {
exportCsvPath: {
default: '',
},
- fullPath: {
+ groupEpicsPath: {
default: '',
},
+ hasBlockedIssuesFeature: {
+ default: false,
+ },
hasIssues: {
default: false,
},
+ hasIssueWeightsFeature: {
+ default: false,
+ },
+ hasMultipleIssueAssigneesFeature: {
+ default: false,
+ },
+ initialEmail: {
+ default: '',
+ },
isSignedIn: {
default: false,
},
@@ -99,6 +134,18 @@ export default {
newIssuePath: {
default: '',
},
+ projectIterationsPath: {
+ default: '',
+ },
+ projectLabelsPath: {
+ default: '',
+ },
+ projectMilestonesPath: {
+ default: '',
+ },
+ projectPath: {
+ default: '',
+ },
rssPath: {
default: '',
},
@@ -110,51 +157,143 @@ export default {
},
},
data() {
- const orderBy = getParameterByName('order_by');
- const sort = getParameterByName('sort');
- const sortKey = Object.keys(sortParams).find(
- (key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort,
- );
-
- const search = getParameterByName('search') || '';
- const tokens = search.split(' ').map((searchWord) => ({
- type: 'filtered-search-term',
- value: {
- data: searchWord,
- },
- }));
+ const state = getParameterByName(PARAM_STATE);
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
return {
+ dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filters: sortParams[sortKey] || {},
- filterTokens: tokens,
+ filterTokens: getFilterTokens(window.location.search),
isLoading: false,
issues: [],
- page: toNumber(getParameterByName('page')) || 1,
+ page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
showBulkEditSidebar: false,
- sortKey: sortKey || CREATED_DESC,
- state: getParameterByName('state') || IssuableStates.Opened,
+ sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
+ state: state || IssuableStates.Opened,
totalIssues: 0,
};
},
computed: {
+ isBulkEditButtonDisabled() {
+ return this.showBulkEditSidebar || !this.issues.length;
+ },
isManualOrdering() {
- return this.sortKey === RELATIVE_POSITION_ASC;
+ return this.sortKey === RELATIVE_POSITION_DESC;
},
isOpenTab() {
return this.state === IssuableStates.Opened;
},
+ apiFilterParams() {
+ return convertToParams(this.filterTokens, API_PARAM);
+ },
+ urlFilterParams() {
+ return convertToParams(this.filterTokens, URL_PARAM);
+ },
searchQuery() {
- return (
- this.filterTokens
- .map((searchTerm) => searchTerm.value.data)
- .filter((searchWord) => Boolean(searchWord))
- .join(' ') || undefined
- );
+ return convertToSearchQuery(this.filterTokens) || undefined;
+ },
+ searchTokens() {
+ const tokens = [
+ {
+ type: 'author_username',
+ title: TOKEN_TITLE_AUTHOR,
+ icon: 'pencil',
+ token: AuthorToken,
+ dataType: 'user',
+ unique: true,
+ defaultAuthors: [],
+ fetchAuthors: this.fetchUsers,
+ },
+ {
+ type: 'assignee_username',
+ title: TOKEN_TITLE_ASSIGNEE,
+ icon: 'user',
+ token: AuthorToken,
+ dataType: 'user',
+ unique: !this.hasMultipleIssueAssigneesFeature,
+ defaultAuthors: DEFAULT_NONE_ANY,
+ fetchAuthors: this.fetchUsers,
+ },
+ {
+ type: 'milestone',
+ title: TOKEN_TITLE_MILESTONE,
+ icon: 'clock',
+ token: MilestoneToken,
+ unique: true,
+ defaultMilestones: [],
+ fetchMilestones: this.fetchMilestones,
+ },
+ {
+ type: 'labels',
+ title: TOKEN_TITLE_LABEL,
+ icon: 'labels',
+ token: LabelToken,
+ defaultLabels: [],
+ fetchLabels: this.fetchLabels,
+ },
+ {
+ type: 'my_reaction_emoji',
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ fetchEmojis: this.fetchEmojis,
+ },
+ {
+ type: 'confidential',
+ title: TOKEN_TITLE_CONFIDENTIAL,
+ icon: 'eye-slash',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ options: [
+ { icon: 'eye-slash', value: 'yes', title: this.$options.i18n.confidentialYes },
+ { icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
+ ],
+ },
+ ];
+
+ if (this.projectIterationsPath) {
+ tokens.push({
+ type: 'iteration',
+ title: TOKEN_TITLE_ITERATION,
+ icon: 'iteration',
+ token: IterationToken,
+ unique: true,
+ fetchIterations: this.fetchIterations,
+ });
+ }
+
+ if (this.groupEpicsPath) {
+ tokens.push({
+ type: 'epic_id',
+ title: TOKEN_TITLE_EPIC,
+ icon: 'epic',
+ token: EpicToken,
+ unique: true,
+ fetchEpics: this.fetchEpics,
+ });
+ }
+
+ if (this.hasIssueWeightsFeature) {
+ tokens.push({
+ type: 'weight',
+ title: TOKEN_TITLE_WEIGHT,
+ icon: 'weight',
+ token: WeightToken,
+ unique: true,
+ });
+ }
+
+ return tokens;
},
showPaginationControls() {
return this.issues.length > 0;
},
+ sortOptions() {
+ return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
+ },
tabCounts() {
return Object.values(IssuableStates).reduce(
(acc, state) => ({
@@ -166,24 +305,65 @@ export default {
},
urlParams() {
return {
+ due_date: this.dueDateFilter,
page: this.page,
search: this.searchQuery,
state: this.state,
- ...this.filters,
+ ...urlSortParams[this.sortKey],
+ ...this.urlFilterParams,
};
},
},
+ created() {
+ this.cache = {};
+ },
mounted() {
- eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
- this.showBulkEditSidebar = showBulkEditSidebar;
- });
+ eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
this.fetchIssues();
},
beforeDestroy() {
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('issuables:toggleBulkEdit');
+ eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
},
methods: {
+ fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
+ if (this.cache[cacheName]) {
+ const data = search
+ ? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
+ : this.cache[cacheName].slice(0, MAX_LIST_SIZE);
+ return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
+ }
+
+ return axios.get(path).then(({ data }) => {
+ this.cache[cacheName] = data;
+ const result = data.slice(0, MAX_LIST_SIZE);
+ return wrapData ? { data: result } : result;
+ });
+ },
+ fetchEmojis(search) {
+ return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
+ },
+ async fetchEpics(search) {
+ const epics = await this.fetchWithCache(this.groupEpicsPath, 'epics');
+ if (!search) {
+ return epics.slice(0, MAX_LIST_SIZE);
+ }
+ const number = Number(search);
+ return Number.isNaN(number)
+ ? fuzzaldrinPlus.filter(epics, search, { key: 'title' })
+ : epics.filter((epic) => epic.id === number);
+ },
+ fetchLabels(search) {
+ return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search);
+ },
+ fetchMilestones(search) {
+ return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
+ },
+ fetchIterations(search) {
+ return axios.get(this.projectIterationsPath, { params: { search } });
+ },
+ fetchUsers(search) {
+ return axios.get(this.autocompleteUsersPath, { params: { search } });
+ },
fetchIssues() {
if (!this.hasIssues) {
return undefined;
@@ -194,12 +374,14 @@ export default {
return axios
.get(this.endpoint, {
params: {
+ due_date: this.dueDateFilter,
page: this.page,
- per_page: this.$options.PAGE_SIZE,
+ per_page: PAGE_SIZE,
search: this.searchQuery,
state: this.state,
with_labels_details: true,
- ...this.filters,
+ ...apiSortParams[this.sortKey],
+ ...this.apiFilterParams,
},
})
.then(({ data, headers }) => {
@@ -209,7 +391,7 @@ export default {
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
})
.catch(() => {
- createFlash({ message: __('An error occurred while loading issues') });
+ createFlash({ message: this.$options.i18n.errorFetchingIssues });
})
.finally(() => {
this.isLoading = false;
@@ -218,6 +400,15 @@ export default {
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
+ getStatus(issue) {
+ if (issue.closedAt && issue.movedToId) {
+ return this.$options.i18n.closedMoved;
+ }
+ if (issue.closedAt) {
+ return this.$options.i18n.closed;
+ }
+ return undefined;
+ },
handleUpdateLegacyBulkEdit() {
// If "select all" checkbox was checked, wait for all checkboxes
// to be checked before updating IssuableBulkUpdateSidebar class
@@ -225,7 +416,18 @@ export default {
eventHub.$emit('issuables:updateBulkEdit');
});
},
- handleBulkUpdateClick() {
+ async handleBulkUpdateClick() {
+ if (!this.hasInitBulkEdit) {
+ const initBulkUpdateSidebar = await import('~/issuable_init_bulk_update_sidebar');
+ initBulkUpdateSidebar.default.init('issuable_');
+
+ const usersSelect = await import('~/users_select');
+ const UsersSelect = usersSelect.default;
+ new UsersSelect(); // eslint-disable-line no-new
+
+ this.hasInitBulkEdit = true;
+ }
+
eventHub.$emit('issuables:enableBulkEdit');
},
handleClickTab(state) {
@@ -278,151 +480,161 @@ export default {
},
handleSort(value) {
this.sortKey = value;
- this.filters = sortParams[value];
this.fetchIssues();
},
+ toggleBulkEditSidebar(showBulkEditSidebar) {
+ this.showBulkEditSidebar = showBulkEditSidebar;
+ },
},
};
</script>
<template>
- <issuable-list
- v-if="hasIssues"
- :namespace="fullPath"
- recent-searches-storage-key="issues"
- :search-input-placeholder="__('Search or filter results…')"
- :search-tokens="[]"
- :initial-filter-value="filterTokens"
- :sort-options="$options.sortOptions"
- :initial-sort-by="sortKey"
- :issuables="issues"
- :tabs="$options.IssuableListTabs"
- :current-tab="state"
- :tab-counts="tabCounts"
- :issuables-loading="isLoading"
- :is-manual-ordering="isManualOrdering"
- :show-bulk-edit-sidebar="showBulkEditSidebar"
- :show-pagination-controls="showPaginationControls"
- :total-items="totalIssues"
- :current-page="page"
- :previous-page="page - 1"
- :next-page="page + 1"
- :url-params="urlParams"
- @click-tab="handleClickTab"
- @filter="handleFilter"
- @page-change="handlePageChange"
- @reorder="handleReorder"
- @sort="handleSort"
- @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
- >
- <template #nav-actions>
- <gl-button
- v-gl-tooltip
- :href="rssPath"
- icon="rss"
- :title="$options.i18n.rssLabel"
- :aria-label="$options.i18n.rssLabel"
- />
- <gl-button
- v-gl-tooltip
- :href="calendarPath"
- icon="calendar"
- :title="$options.i18n.calendarLabel"
- :aria-label="$options.i18n.calendarLabel"
- />
- <csv-import-export-buttons
- class="gl-mr-3"
- :export-csv-path="exportCsvPathWithQuery"
- :issuable-count="totalIssues"
- />
- <gl-button
- v-if="canBulkUpdate"
- :disabled="showBulkEditSidebar"
- @click="handleBulkUpdateClick"
- >
- {{ __('Edit issues') }}
- </gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
+ <div v-if="hasIssues">
+ <issuable-list
+ :namespace="projectPath"
+ recent-searches-storage-key="issues"
+ :search-input-placeholder="$options.i18n.searchPlaceholder"
+ :search-tokens="searchTokens"
+ :initial-filter-value="filterTokens"
+ :sort-options="sortOptions"
+ :initial-sort-by="sortKey"
+ :issuables="issues"
+ :tabs="$options.IssuableListTabs"
+ :current-tab="state"
+ :tab-counts="tabCounts"
+ :issuables-loading="isLoading"
+ :is-manual-ordering="isManualOrdering"
+ :show-bulk-edit-sidebar="showBulkEditSidebar"
+ :show-pagination-controls="showPaginationControls"
+ :total-items="totalIssues"
+ :current-page="page"
+ :previous-page="page - 1"
+ :next-page="page + 1"
+ :url-params="urlParams"
+ @click-tab="handleClickTab"
+ @filter="handleFilter"
+ @page-change="handlePageChange"
+ @reorder="handleReorder"
+ @sort="handleSort"
+ @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
+ >
+ <template #nav-actions>
+ <gl-button
+ v-gl-tooltip
+ :href="rssPath"
+ icon="rss"
+ :title="$options.i18n.rssLabel"
+ :aria-label="$options.i18n.rssLabel"
+ />
+ <gl-button
+ v-gl-tooltip
+ :href="calendarPath"
+ icon="calendar"
+ :title="$options.i18n.calendarLabel"
+ :aria-label="$options.i18n.calendarLabel"
+ />
+ <csv-import-export-buttons
+ v-if="isSignedIn"
+ class="gl-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="totalIssues"
+ />
+ <gl-button
+ v-if="canBulkUpdate"
+ :disabled="isBulkEditButtonDisabled"
+ @click="handleBulkUpdateClick"
+ >
+ {{ $options.i18n.editIssues }}
+ </gl-button>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
- <template #timeframe="{ issuable = {} }">
- <issue-card-time-info :issue="issuable" />
- </template>
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
- <template #statistics="{ issuable = {} }">
- <li
- v-if="issuable.mergeRequestsCount"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
- :title="__('Related merge requests')"
- data-testid="issuable-mr"
- >
- <gl-icon name="merge-request" />
- {{ issuable.mergeRequestsCount }}
- </li>
- <li
- v-if="issuable.upvotes"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
- :title="__('Upvotes')"
- data-testid="issuable-upvotes"
- >
- <gl-icon name="thumb-up" />
- {{ issuable.upvotes }}
- </li>
- <li
- v-if="issuable.downvotes"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
- :title="__('Downvotes')"
- data-testid="issuable-downvotes"
- >
- <gl-icon name="thumb-down" />
- {{ issuable.downvotes }}
- </li>
- <blocking-issues-count
- class="gl-display-none gl-sm-display-block"
- :blocking-issues-count="issuable.blockingIssuesCount"
- :is-list-item="true"
- />
- </template>
+ <template #status="{ issuable = {} }">
+ {{ getStatus(issuable) }}
+ </template>
- <template #empty-state>
- <gl-empty-state
- v-if="searchQuery"
- :description="$options.i18n.noSearchResultsDescription"
- :title="$options.i18n.noSearchResultsTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
+ <template #statistics="{ issuable = {} }">
+ <li
+ v-if="issuable.mergeRequestsCount"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block"
+ :title="$options.i18n.relatedMergeRequests"
+ data-testid="issuable-mr"
+ >
+ <gl-icon name="merge-request" />
+ {{ issuable.mergeRequestsCount }}
+ </li>
+ <li
+ v-if="issuable.upvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block"
+ :title="$options.i18n.upvotes"
+ data-testid="issuable-upvotes"
+ >
+ <gl-icon name="thumb-up" />
+ {{ issuable.upvotes }}
+ </li>
+ <li
+ v-if="issuable.downvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block"
+ :title="$options.i18n.downvotes"
+ data-testid="issuable-downvotes"
+ >
+ <gl-icon name="thumb-down" />
+ {{ issuable.downvotes }}
+ </li>
+ <blocking-issues-count
+ class="gl-display-none gl-sm-display-block"
+ :blocking-issues-count="issuable.blockingIssuesCount"
+ :is-list-item="true"
+ />
+ </template>
- <gl-empty-state
- v-else-if="isOpenTab"
- :description="$options.i18n.noOpenIssuesDescription"
- :title="$options.i18n.noOpenIssuesTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
+ <template #empty-state>
+ <gl-empty-state
+ v-if="searchQuery"
+ :description="$options.i18n.noSearchResultsDescription"
+ :title="$options.i18n.noSearchResultsTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
- <gl-empty-state
- v-else
- :title="$options.i18n.noClosedIssuesTitle"
- :svg-path="emptyStateSvgPath"
- />
- </template>
- </issuable-list>
+ <gl-empty-state
+ v-else-if="isOpenTab"
+ :description="$options.i18n.noOpenIssuesDescription"
+ :title="$options.i18n.noOpenIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.noClosedIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ />
+ </template>
+ </issuable-list>
+
+ <issuable-by-email v-if="initialEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
+ </div>
<div v-else-if="isSignedIn">
<gl-empty-state
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index f6f23af80ba..54e9668d300 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -1,4 +1,11 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import {
+ FILTER_ANY,
+ FILTER_CURRENT,
+ FILTER_NONE,
+ OPERATOR_IS,
+ OPERATOR_IS_NOT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
// Maps sort order as it appears in the URL query to API `order_by` and `sort` params.
const PRIORITY = 'priority';
@@ -53,23 +60,78 @@ export const availableSortOptionsJira = [
},
];
+export const i18n = {
+ calendarLabel: __('Subscribe to calendar'),
+ closed: __('CLOSED'),
+ closedMoved: __('CLOSED (MOVED)'),
+ confidentialNo: __('No'),
+ confidentialYes: __('Yes'),
+ downvotes: __('Downvotes'),
+ editIssues: __('Edit issues'),
+ errorFetchingIssues: __('An error occurred while loading issues'),
+ jiraIntegrationMessage: s__(
+ 'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
+ ),
+ jiraIntegrationSecondaryMessage: s__('JiraService|This feature requires a Premium plan.'),
+ jiraIntegrationTitle: s__('JiraService|Using Jira for issue tracking?'),
+ newIssueLabel: __('New issue'),
+ noClosedIssuesTitle: __('There are no closed issues'),
+ noOpenIssuesDescription: __('To keep this project going, create a new issue'),
+ noOpenIssuesTitle: __('There are no open issues'),
+ noIssuesSignedInDescription: __(
+ 'Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.',
+ ),
+ noIssuesSignedInTitle: __(
+ 'The Issue Tracker is the place to add things that need to be improved or solved in a project',
+ ),
+ noIssuesSignedOutButtonText: __('Register / Sign In'),
+ noIssuesSignedOutDescription: __(
+ 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
+ ),
+ noIssuesSignedOutTitle: __('There are no issues to show'),
+ noSearchResultsDescription: __('To widen your search, change or remove filters above'),
+ noSearchResultsTitle: __('Sorry, your filter produced no results'),
+ relatedMergeRequests: __('Related merge requests'),
+ reorderError: __('An error occurred while reordering issues.'),
+ rssLabel: __('Subscribe to RSS feed'),
+ searchPlaceholder: __('Search or filter results…'),
+ upvotes: __('Upvotes'),
+};
+
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
-export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
+export const PARAM_DUE_DATE = 'due_date';
+export const PARAM_PAGE = 'page';
+export const PARAM_SORT = 'sort';
+export const PARAM_STATE = 'state';
+
+export const DUE_DATE_NONE = '0';
+export const DUE_DATE_ANY = '';
+export const DUE_DATE_OVERDUE = 'overdue';
+export const DUE_DATE_WEEK = 'week';
+export const DUE_DATE_MONTH = 'month';
+export const DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS = 'next_month_and_previous_two_weeks';
+export const DUE_DATE_VALUES = [
+ DUE_DATE_NONE,
+ DUE_DATE_ANY,
+ DUE_DATE_OVERDUE,
+ DUE_DATE_WEEK,
+ DUE_DATE_MONTH,
+ DUE_DATE_NEXT_MONTH_AND_PREVIOUS_TWO_WEEKS,
+];
+
export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
export const CREATED_ASC = 'CREATED_ASC';
export const CREATED_DESC = 'CREATED_DESC';
export const DUE_DATE_ASC = 'DUE_DATE_ASC';
export const DUE_DATE_DESC = 'DUE_DATE_DESC';
-export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
export const POPULARITY_ASC = 'POPULARITY_ASC';
export const POPULARITY_DESC = 'POPULARITY_DESC';
-export const PRIORITY_ASC = 'PRIORITY_ASC';
export const PRIORITY_DESC = 'PRIORITY_DESC';
-export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
+export const RELATIVE_POSITION_DESC = 'RELATIVE_POSITION_DESC';
export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
@@ -78,13 +140,19 @@ export const WEIGHT_DESC = 'WEIGHT_DESC';
const SORT_ASC = 'asc';
const SORT_DESC = 'desc';
+const CREATED_DATE_SORT = 'created_date';
+const CREATED_ASC_SORT = 'created_asc';
+const UPDATED_DESC_SORT = 'updated_desc';
+const UPDATED_ASC_SORT = 'updated_asc';
+const MILESTONE_SORT = 'milestone';
+const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
+const DUE_DATE_DESC_SORT = 'due_date_desc';
+const POPULARITY_ASC_SORT = 'popularity_asc';
+const WEIGHT_DESC_SORT = 'weight_desc';
+const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
const BLOCKING_ISSUES = 'blocking_issues';
-export const sortParams = {
- [PRIORITY_ASC]: {
- order_by: PRIORITY,
- sort: SORT_ASC,
- },
+export const apiSortParams = {
[PRIORITY_DESC]: {
order_by: PRIORITY,
sort: SORT_DESC,
@@ -129,15 +197,11 @@ export const sortParams = {
order_by: POPULARITY,
sort: SORT_DESC,
},
- [LABEL_PRIORITY_ASC]: {
- order_by: LABEL_PRIORITY,
- sort: SORT_ASC,
- },
[LABEL_PRIORITY_DESC]: {
order_by: LABEL_PRIORITY,
sort: SORT_DESC,
},
- [RELATIVE_POSITION_ASC]: {
+ [RELATIVE_POSITION_DESC]: {
order_by: RELATIVE_POSITION,
per_page: 100,
sort: SORT_ASC,
@@ -150,95 +214,233 @@ export const sortParams = {
order_by: WEIGHT,
sort: SORT_DESC,
},
- [BLOCKING_ISSUES_ASC]: {
- order_by: BLOCKING_ISSUES,
- sort: SORT_ASC,
- },
[BLOCKING_ISSUES_DESC]: {
order_by: BLOCKING_ISSUES,
sort: SORT_DESC,
},
};
-export const sortOptions = [
- {
- id: 1,
- title: __('Priority'),
- sortDirection: {
- ascending: PRIORITY_ASC,
- descending: PRIORITY_DESC,
- },
+export const urlSortParams = {
+ [PRIORITY_DESC]: {
+ sort: PRIORITY,
},
- {
- id: 2,
- title: __('Created date'),
- sortDirection: {
- ascending: CREATED_ASC,
- descending: CREATED_DESC,
+ [CREATED_ASC]: {
+ sort: CREATED_ASC_SORT,
+ },
+ [CREATED_DESC]: {
+ sort: CREATED_DATE_SORT,
+ },
+ [UPDATED_ASC]: {
+ sort: UPDATED_ASC_SORT,
+ },
+ [UPDATED_DESC]: {
+ sort: UPDATED_DESC_SORT,
+ },
+ [MILESTONE_DUE_ASC]: {
+ sort: MILESTONE_SORT,
+ },
+ [MILESTONE_DUE_DESC]: {
+ sort: MILESTONE_DUE_DESC_SORT,
+ },
+ [DUE_DATE_ASC]: {
+ sort: DUE_DATE,
+ },
+ [DUE_DATE_DESC]: {
+ sort: DUE_DATE_DESC_SORT,
+ },
+ [POPULARITY_ASC]: {
+ sort: POPULARITY_ASC_SORT,
+ },
+ [POPULARITY_DESC]: {
+ sort: POPULARITY,
+ },
+ [LABEL_PRIORITY_DESC]: {
+ sort: LABEL_PRIORITY,
+ },
+ [RELATIVE_POSITION_DESC]: {
+ sort: RELATIVE_POSITION,
+ per_page: 100,
+ },
+ [WEIGHT_ASC]: {
+ sort: WEIGHT,
+ },
+ [WEIGHT_DESC]: {
+ sort: WEIGHT_DESC_SORT,
+ },
+ [BLOCKING_ISSUES_DESC]: {
+ sort: BLOCKING_ISSUES_DESC_SORT,
+ },
+};
+
+export const MAX_LIST_SIZE = 10;
+
+export const API_PARAM = 'apiParam';
+export const URL_PARAM = 'urlParam';
+export const NORMAL_FILTER = 'normalFilter';
+export const SPECIAL_FILTER = 'specialFilter';
+export const ALTERNATIVE_FILTER = 'alternativeFilter';
+export const SPECIAL_FILTER_VALUES = [FILTER_NONE, FILTER_ANY, FILTER_CURRENT];
+
+export const filters = {
+ author_username: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'author_username',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[author_username]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'author_username',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[author_username]',
+ },
},
},
- {
- id: 3,
- title: __('Last updated'),
- sortDirection: {
- ascending: UPDATED_ASC,
- descending: UPDATED_DESC,
+ assignee_username: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'assignee_username',
+ [SPECIAL_FILTER]: 'assignee_id',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[assignee_username]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'assignee_username[]',
+ [SPECIAL_FILTER]: 'assignee_id',
+ [ALTERNATIVE_FILTER]: 'assignee_username',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[assignee_username][]',
+ },
},
},
- {
- id: 4,
- title: __('Milestone due date'),
- sortDirection: {
- ascending: MILESTONE_DUE_ASC,
- descending: MILESTONE_DUE_DESC,
+ milestone: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'milestone',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[milestone]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'milestone_title',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[milestone_title]',
+ },
},
},
- {
- id: 5,
- title: __('Due date'),
- sortDirection: {
- ascending: DUE_DATE_ASC,
- descending: DUE_DATE_DESC,
+ labels: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'labels',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[labels]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'label_name[]',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[label_name][]',
+ },
},
},
- {
- id: 6,
- title: __('Popularity'),
- sortDirection: {
- ascending: POPULARITY_ASC,
- descending: POPULARITY_DESC,
+ my_reaction_emoji: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'my_reaction_emoji',
+ [SPECIAL_FILTER]: 'my_reaction_emoji',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'my_reaction_emoji',
+ [SPECIAL_FILTER]: 'my_reaction_emoji',
+ },
},
},
- {
- id: 7,
- title: __('Label priority'),
- sortDirection: {
- ascending: LABEL_PRIORITY_ASC,
- descending: LABEL_PRIORITY_DESC,
+ confidential: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'confidential',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'confidential',
+ },
},
},
- {
- id: 8,
- title: __('Manual'),
- sortDirection: {
- ascending: RELATIVE_POSITION_ASC,
- descending: RELATIVE_POSITION_ASC,
+ iteration: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'iteration_title',
+ [SPECIAL_FILTER]: 'iteration_id',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[iteration_title]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'iteration_title',
+ [SPECIAL_FILTER]: 'iteration_id',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[iteration_title]',
+ },
},
},
- {
- id: 9,
- title: __('Weight'),
- sortDirection: {
- ascending: WEIGHT_ASC,
- descending: WEIGHT_DESC,
+ epic_id: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'epic_id',
+ [SPECIAL_FILTER]: 'epic_id',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[epic_id]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'epic_id',
+ [SPECIAL_FILTER]: 'epic_id',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[epic_id]',
+ },
},
},
- {
- id: 10,
- title: __('Blocking'),
- sortDirection: {
- ascending: BLOCKING_ISSUES_ASC,
- descending: BLOCKING_ISSUES_DESC,
+ weight: {
+ [API_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'weight',
+ [SPECIAL_FILTER]: 'weight',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[weight]',
+ },
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'weight',
+ [SPECIAL_FILTER]: 'weight',
+ },
+ [OPERATOR_IS_NOT]: {
+ [NORMAL_FILTER]: 'not[weight]',
+ },
},
},
-];
+};
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 0b64df50691..55719f6449b 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { IssuableType } from '~/issue_show/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import IssuablesListApp from './components/issuables_list_app.vue';
import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
-function mountJiraIssuesListApp() {
+export function mountJiraIssuesListApp() {
const el = document.querySelector('.js-jira-issues-import-status');
if (!el) {
@@ -36,7 +37,7 @@ function mountJiraIssuesListApp() {
});
}
-function mountIssuablesListApp() {
+export function mountIssuablesListApp() {
if (!gon.features?.vueIssuablesList) {
return;
}
@@ -65,7 +66,7 @@ function mountIssuablesListApp() {
});
}
-export function initIssuesListApp() {
+export function mountIssuesListApp() {
const el = document.querySelector('.js-issues-list');
if (!el) {
@@ -73,26 +74,38 @@ export function initIssuesListApp() {
}
const {
+ autocompleteAwardEmojisPath,
+ autocompleteUsersPath,
calendarPath,
canBulkUpdate,
canEdit,
canImportIssues,
email,
+ emailsHelpPagePath,
emptyStateSvgPath,
endpoint,
exportCsvPath,
- fullPath,
+ groupEpicsPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssues,
hasIssueWeightsFeature,
+ hasMultipleIssueAssigneesFeature,
importCsvIssuesPath,
+ initialEmail,
isSignedIn,
issuesPath,
jiraIntegrationPath,
+ markdownHelpPath,
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
+ projectIterationsPath,
+ projectLabelsPath,
+ projectMilestonesPath,
+ projectPath,
+ quickActionsHelpPath,
+ resetPath,
rssPath,
showNewIssueLink,
signInPath,
@@ -104,19 +117,26 @@ export function initIssuesListApp() {
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider: {},
provide: {
+ autocompleteAwardEmojisPath,
+ autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
endpoint,
- fullPath,
+ groupEpicsPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssues: parseBoolean(hasIssues),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
isSignedIn: parseBoolean(isSignedIn),
issuesPath,
jiraIntegrationPath,
newIssuePath,
+ projectIterationsPath,
+ projectLabelsPath,
+ projectMilestonesPath,
+ projectPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
@@ -130,12 +150,14 @@ export function initIssuesListApp() {
showExportButton: parseBoolean(hasIssues),
showImportButton: parseBoolean(canImportIssues),
showLabel: !parseBoolean(hasIssues),
+ // For IssuableByEmail component
+ emailsHelpPagePath,
+ initialEmail,
+ issuableType: IssuableType.Issue,
+ markdownHelpPath,
+ quickActionsHelpPath,
+ resetPath,
},
render: (createComponent) => createComponent(IssuesListApp),
});
}
-
-export default function initIssuablesList() {
- mountJiraIssuesListApp();
- mountIssuablesListApp();
-}
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
new file mode 100644
index 00000000000..234fd59ca8d
--- /dev/null
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -0,0 +1,195 @@
+import {
+ BLOCKING_ISSUES_DESC,
+ CREATED_ASC,
+ CREATED_DESC,
+ DUE_DATE_ASC,
+ DUE_DATE_DESC,
+ DUE_DATE_VALUES,
+ filters,
+ LABEL_PRIORITY_DESC,
+ MILESTONE_DUE_ASC,
+ MILESTONE_DUE_DESC,
+ NORMAL_FILTER,
+ POPULARITY_ASC,
+ POPULARITY_DESC,
+ PRIORITY_DESC,
+ RELATIVE_POSITION_DESC,
+ SPECIAL_FILTER,
+ SPECIAL_FILTER_VALUES,
+ UPDATED_ASC,
+ UPDATED_DESC,
+ urlSortParams,
+ WEIGHT_ASC,
+ WEIGHT_DESC,
+} from '~/issues_list/constants';
+import { isPositiveInteger } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
+export const getSortKey = (sort) =>
+ Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
+
+export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
+
+export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
+ const sortOptions = [
+ {
+ id: 1,
+ title: __('Priority'),
+ sortDirection: {
+ ascending: PRIORITY_DESC,
+ descending: PRIORITY_DESC,
+ },
+ },
+ {
+ id: 2,
+ title: __('Created date'),
+ sortDirection: {
+ ascending: CREATED_ASC,
+ descending: CREATED_DESC,
+ },
+ },
+ {
+ id: 3,
+ title: __('Last updated'),
+ sortDirection: {
+ ascending: UPDATED_ASC,
+ descending: UPDATED_DESC,
+ },
+ },
+ {
+ id: 4,
+ title: __('Milestone due date'),
+ sortDirection: {
+ ascending: MILESTONE_DUE_ASC,
+ descending: MILESTONE_DUE_DESC,
+ },
+ },
+ {
+ id: 5,
+ title: __('Due date'),
+ sortDirection: {
+ ascending: DUE_DATE_ASC,
+ descending: DUE_DATE_DESC,
+ },
+ },
+ {
+ id: 6,
+ title: __('Popularity'),
+ sortDirection: {
+ ascending: POPULARITY_ASC,
+ descending: POPULARITY_DESC,
+ },
+ },
+ {
+ id: 7,
+ title: __('Label priority'),
+ sortDirection: {
+ ascending: LABEL_PRIORITY_DESC,
+ descending: LABEL_PRIORITY_DESC,
+ },
+ },
+ {
+ id: 8,
+ title: __('Manual'),
+ sortDirection: {
+ ascending: RELATIVE_POSITION_DESC,
+ descending: RELATIVE_POSITION_DESC,
+ },
+ },
+ ];
+
+ if (hasIssueWeightsFeature) {
+ sortOptions.push({
+ id: 9,
+ title: __('Weight'),
+ sortDirection: {
+ ascending: WEIGHT_ASC,
+ descending: WEIGHT_DESC,
+ },
+ });
+ }
+
+ if (hasBlockedIssuesFeature) {
+ sortOptions.push({
+ id: 10,
+ title: __('Blocking'),
+ sortDirection: {
+ ascending: BLOCKING_ISSUES_DESC,
+ descending: BLOCKING_ISSUES_DESC,
+ },
+ });
+ }
+
+ return sortOptions;
+};
+
+const tokenTypes = Object.keys(filters);
+
+const getUrlParams = (tokenType) =>
+ Object.values(filters[tokenType].urlParam).flatMap((filterObj) => Object.values(filterObj));
+
+const urlParamKeys = tokenTypes.flatMap(getUrlParams);
+
+const getTokenTypeFromUrlParamKey = (urlParamKey) =>
+ tokenTypes.find((tokenType) => getUrlParams(tokenType).includes(urlParamKey));
+
+const getOperatorFromUrlParamKey = (tokenType, urlParamKey) =>
+ Object.entries(filters[tokenType].urlParam).find(([, filterObj]) =>
+ Object.values(filterObj).includes(urlParamKey),
+ )[0];
+
+const convertToFilteredTokens = (locationSearch) =>
+ Array.from(new URLSearchParams(locationSearch).entries())
+ .filter(([key]) => urlParamKeys.includes(key))
+ .map(([key, data]) => {
+ const type = getTokenTypeFromUrlParamKey(key);
+ const operator = getOperatorFromUrlParamKey(type, key);
+ return {
+ type,
+ value: { data, operator },
+ };
+ });
+
+const convertToFilteredSearchTerms = (locationSearch) =>
+ new URLSearchParams(locationSearch)
+ .get('search')
+ ?.split(' ')
+ .map((word) => ({
+ type: FILTERED_SEARCH_TERM,
+ value: {
+ data: word,
+ },
+ })) || [];
+
+export const getFilterTokens = (locationSearch) => {
+ if (!locationSearch) {
+ return [];
+ }
+ const filterTokens = convertToFilteredTokens(locationSearch);
+ const searchTokens = convertToFilteredSearchTerms(locationSearch);
+ return filterTokens.concat(searchTokens);
+};
+
+const getFilterType = (data, tokenType = '') =>
+ SPECIAL_FILTER_VALUES.includes(data) ||
+ (tokenType === 'assignee_username' && isPositiveInteger(data))
+ ? SPECIAL_FILTER
+ : NORMAL_FILTER;
+
+export const convertToParams = (filterTokens, paramType) =>
+ filterTokens
+ .filter((token) => token.type !== FILTERED_SEARCH_TERM)
+ .reduce((acc, token) => {
+ const filterType = getFilterType(token.value.data, token.type);
+ const param = filters[token.type][paramType][token.value.operator]?.[filterType];
+ return Object.assign(acc, {
+ [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
+ });
+ }, {});
+
+export const convertToSearchQuery = (filterTokens) =>
+ filterTokens
+ .filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data)
+ .map((token) => token.value.data)
+ .join(' ');
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index 275ff820419..d764f778a9d 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
import { fetchGroups } from '~/jira_connect/api';
-import { defaultPerPage } from '~/jira_connect/constants';
+import { DEFAULT_GROUPS_PER_PAGE, MINIMUM_SEARCH_TERM_LENGTH } from '~/jira_connect/constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import GroupsListItem from './groups_list_item.vue';
@@ -25,24 +25,33 @@ export default {
isLoadingInitial: true,
isLoadingMore: false,
page: 1,
- perPage: defaultPerPage,
totalItems: 0,
errorMessage: null,
+ searchTerm: '',
};
},
+ computed: {
+ showPagination() {
+ return this.totalItems > this.$options.DEFAULT_GROUPS_PER_PAGE && this.groups.length > 0;
+ },
+ },
mounted() {
return this.loadGroups().finally(() => {
this.isLoadingInitial = false;
});
},
methods: {
- loadGroups({ searchTerm } = {}) {
- this.isLoadingMore = true;
+ loadGroups() {
+ // fetchGroups returns no results for search terms 0 < {length} < 3.
+ // The desired UX is to return the unfiltered results for searches {length} < 3.
+ // Here, we set the search to an empty string if {length} < 3
+ const search = this.searchTerm?.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.searchTerm;
+ this.isLoadingMore = true;
return fetchGroups(this.groupsPath, {
page: this.page,
- perPage: this.perPage,
- search: searchTerm,
+ perPage: this.$options.DEFAULT_GROUPS_PER_PAGE,
+ search,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
@@ -58,9 +67,14 @@ export default {
});
},
onGroupSearch(searchTerm) {
- return this.loadGroups({ searchTerm });
+ // keep a copy of the search term for pagination
+ this.searchTerm = searchTerm;
+ // reset the current page
+ this.page = 1;
+ return this.loadGroups();
},
},
+ DEFAULT_GROUPS_PER_PAGE,
};
</script>
@@ -102,10 +116,10 @@ export default {
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
- v-if="totalItems > perPage && groups.length > 0"
+ v-if="showPagination"
v-model="page"
class="gl-mb-0"
- :per-page="perPage"
+ :per-page="$options.DEFAULT_GROUPS_PER_PAGE"
:total-items="totalItems"
@input="loadGroups"
/>
diff --git a/app/assets/javascripts/jira_connect/constants.js b/app/assets/javascripts/jira_connect/constants.js
index 63b79581a1b..8dff83eabb5 100644
--- a/app/assets/javascripts/jira_connect/constants.js
+++ b/app/assets/javascripts/jira_connect/constants.js
@@ -1,2 +1,3 @@
-export const defaultPerPage = 10;
+export const DEFAULT_GROUPS_PER_PAGE = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
+export const MINIMUM_SEARCH_TERM_LENGTH = 3;
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 91ab68d5f39..be95001a396 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
+import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
@@ -32,6 +33,7 @@ export default {
GlLoadingIcon,
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
GlAlert,
+ CodeQualityWalkthrough,
},
directives: {
SafeHtml,
@@ -72,6 +74,11 @@ export default {
required: false,
default: null,
},
+ codeQualityHelpUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
...mapState([
@@ -120,6 +127,10 @@ export default {
shouldRenderHeaderCallout() {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
+
+ shouldRenderCodeQualityWalkthrough() {
+ return this.job.status.group === 'failed-with-warnings';
+ },
},
watch: {
// Once the job log is loaded,
@@ -190,7 +201,7 @@ export default {
</script>
<template>
<div>
- <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation prepend-top-20" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation gl-mt-6" />
<template v-else-if="shouldRenderContent">
<div class="build-page" data-testid="job-content">
@@ -216,6 +227,11 @@ export default {
>
<div v-safe-html="job.callout_message"></div>
</gl-alert>
+ <code-quality-walkthrough
+ v-if="shouldRenderCodeQualityWalkthrough"
+ step="troubleshoot_job"
+ :link="codeQualityHelpUrl"
+ />
</header>
<!-- EO Header Section -->
@@ -256,17 +272,17 @@ export default {
<div
v-if="job.archived"
- class="gl-mt-3 archived-job"
- :class="{ 'sticky-top border-bottom-0': hasTrace }"
+ class="gl-mt-3 gl-py-2 gl-px-3 gl-align-items-center gl-z-index-1 gl-m-auto archived-job"
+ :class="{ 'sticky-top gl-border-bottom-0': hasTrace }"
data-testid="archived-job"
>
- <gl-icon name="lock" class="align-text-bottom" />
+ <gl-icon name="lock" class="gl-vertical-align-bottom" />
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!-- job log -->
<div
v-if="hasTrace"
- class="build-trace-container position-relative"
+ class="build-trace-container gl-relative"
:class="{ 'gl-mt-3': !job.archived }"
>
<log-top-bar
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 00a570fe2f8..c08ac0317b8 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -35,33 +35,40 @@ export default {
return text;
},
+ jobName() {
+ return this.job.name ? this.job.name : this.job.id;
+ },
+ classes() {
+ return {
+ retried: this.job.retried,
+ 'gl-font-weight-bold': this.isActive,
+ };
+ },
+ dataTestId() {
+ return this.isActive ? 'active-job' : null;
+ },
},
};
</script>
<template>
- <div
- class="build-job position-relative"
- :class="{
- retried: job.retried,
- active: isActive,
- }"
- >
+ <div class="build-job gl-relative" :class="classes">
<gl-link
v-gl-tooltip:tooltip-container.left
:href="job.status.details_path"
:title="tooltipText"
- class="js-job-link gl-display-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-center"
+ :data-testid="dataTestId"
>
<gl-icon
v-if="isActive"
name="arrow-right"
- class="js-arrow-right icon-arrow-right position-absolute d-block"
+ class="icon-arrow-right gl-absolute gl-display-block"
/>
<ci-icon :status="job.status" />
- <span class="text-truncate w-100">{{ job.name ? job.name : job.id }}</span>
+ <span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
<gl-icon v-if="job.retried" name="retry" />
</gl-link>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index ea50a11bed6..957e8243f33 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -69,7 +69,10 @@ export default {
<template>
<div class="top-bar">
<!-- truncate information -->
- <div class="truncated-info d-none d-sm-block float-left" data-testid="log-truncated-info">
+ <div
+ class="truncated-info gl-display-none gl-sm-display-block gl-float-left"
+ data-testid="log-truncated-info"
+ >
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
<gl-link
@@ -83,7 +86,7 @@ export default {
</div>
<!-- eo truncate information -->
- <div class="controllers float-right">
+ <div class="controllers gl-float-right">
<!-- links -->
<gl-button
v-if="rawPath"
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
new file mode 100644
index 00000000000..376482b0319
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -0,0 +1,14 @@
+<script>
+export default {
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div></div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
new file mode 100644
index 00000000000..ba5732d3d43
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ iconSize: 12,
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ finishedTime() {
+ return this.job?.finishedAt;
+ },
+ duration() {
+ return this.job?.duration;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationTimeFormatted(duration) }}
+ </div>
+ <div v-if="finishedTime" data-testid="job-finished-time">
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time
+ v-gl-tooltip
+ :title="tooltipTitle(finishedTime)"
+ data-placement="top"
+ data-container="body"
+ >
+ {{ timeFormatted(finishedTime) }}
+ </time>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
new file mode 100644
index 00000000000..88a9f73258f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
@@ -0,0 +1,163 @@
+<script>
+import { GlBadge, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { s__ } from '~/locale';
+import { SUCCESS_STATUS } from '../../../constants';
+
+export default {
+ iconSize: 12,
+ badgeSize: 'sm',
+ i18n: {
+ stuckText: s__('Jobs|Job is stuck. Check runners.'),
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlBadge,
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ const id = getIdFromGraphQLId(this.job.id);
+ return `#${id}`;
+ },
+ jobPath() {
+ return this.job.detailedStatus?.detailsPath;
+ },
+ jobRef() {
+ return this.job?.refName;
+ },
+ jobRefPath() {
+ return this.job?.refPath;
+ },
+ jobTags() {
+ return this.job.tags;
+ },
+ createdByTag() {
+ return this.job.createdByTag;
+ },
+ triggered() {
+ return this.job.triggered;
+ },
+ isManualJob() {
+ return this.job.manualJob;
+ },
+ successfulJob() {
+ return this.job.status === SUCCESS_STATUS;
+ },
+ showAllowedToFailBadge() {
+ return this.job.allowFailure && !this.successfulJob;
+ },
+ isScheduledJob() {
+ return Boolean(this.job.scheduledAt);
+ },
+ canReadJob() {
+ return this.job?.userPermissions?.readBuild;
+ },
+ jobStuck() {
+ return this.job?.stuck;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-text-truncate">
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-gray-500!"
+ :href="jobPath"
+ data-testid="job-id-link"
+ >
+ {{ jobId }}
+ </gl-link>
+
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
+
+ <gl-icon
+ v-if="jobStuck"
+ v-gl-tooltip="$options.i18n.stuckText"
+ name="warning"
+ :size="$options.iconSize"
+ data-testid="stuck-icon"
+ />
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
+ >
+ <div v-if="jobRef" class="gl-max-w-15 gl-text-truncate">
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-weight-bold gl-text-gray-500!"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+
+ <span v-else>{{ __('none') }}</span>
+
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+
+ <gl-link :href="job.commitPath" data-testid="job-sha">{{ job.shortSha }}</gl-link>
+ </div>
+ </div>
+
+ <div>
+ <gl-badge
+ v-for="tag in jobTags"
+ :key="tag"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="job-tag-badge"
+ >
+ {{ tag }}
+ </gl-badge>
+
+ <gl-badge
+ v-if="triggered"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="triggered-job-badge"
+ >{{ s__('Job|triggered') }}
+ </gl-badge>
+ <gl-badge
+ v-if="showAllowedToFailBadge"
+ variant="warning"
+ :size="$options.badgeSize"
+ data-testid="fail-job-badge"
+ >{{ s__('Job|allowed to fail') }}
+ </gl-badge>
+ <gl-badge
+ v-if="isScheduledJob"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="delayed-job-badge"
+ >{{ s__('Job|delayed') }}
+ </gl-badge>
+ <gl-badge
+ v-if="isManualJob"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="manual-job-badge"
+ >
+ {{ s__('Job|manual') }}
+ </gl-badge>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
new file mode 100644
index 00000000000..71f9397f5f5
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAvatar, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+export default {
+ components: {
+ GlAvatar,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pipelineId() {
+ const id = getIdFromGraphQLId(this.job.pipeline.id);
+ return `#${id}`;
+ },
+ pipelinePath() {
+ return this.job.pipeline?.path;
+ },
+ pipelineUserAvatar() {
+ return this.job.pipeline?.user?.avatarUrl;
+ },
+ userPath() {
+ return this.job.pipeline?.user?.webPath;
+ },
+ showAvatar() {
+ return this.job.pipeline?.user;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-text-truncate">
+ <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
+ {{ pipelineId }}
+ </gl-link>
+ <div>
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
index d9e51b0345a..c2104754bad 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
@@ -8,7 +8,20 @@ query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) {
startCursor
}
nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ }
+ }
+ allowFailure
+ status
+ scheduledAt
+ manualJob
+ triggered
+ createdByTag
detailedStatus {
+ detailsPath
+ group
icon
label
text
@@ -46,6 +59,10 @@ query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) {
playable
cancelable
active
+ stuck
+ userPermissions {
+ readBuild
+ }
}
}
}
diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js
index b6b3bb6d379..05d6ebfd6d6 100644
--- a/app/assets/javascripts/jobs/components/table/index.js
+++ b/app/assets/javascripts/jobs/components/table/index.js
@@ -16,13 +16,21 @@ export default (containerId = 'js-jobs-table') => {
return false;
}
- const { fullPath, jobCounts, jobStatuses } = containerEl.dataset;
+ const {
+ fullPath,
+ jobCounts,
+ jobStatuses,
+ pipelineEditorPath,
+ emptyStateSvgPath,
+ } = containerEl.dataset;
return new Vue({
el: containerEl,
apolloProvider,
provide: {
+ emptyStateSvgPath,
fullPath,
+ pipelineEditorPath,
jobStatuses: JSON.parse(jobStatuses),
jobCounts: JSON.parse(jobCounts),
},
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index 32b26d45dfe..4fe5bbf79cd 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -1,57 +1,81 @@
<script>
import { GlTable } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__, __ } from '~/locale';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import ActionsCell from './cells/actions_cell.vue';
+import DurationCell from './cells/duration_cell.vue';
+import JobCell from './cells/job_cell.vue';
+import PipelineCell from './cells/pipeline_cell.vue';
const defaultTableClasses = {
tdClass: 'gl-p-5!',
thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!',
};
+// eslint-disable-next-line @gitlab/require-i18n-strings
+const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
export default {
+ i18n: {
+ emptyText: s__('Jobs|No jobs to show'),
+ },
fields: [
{
key: 'status',
label: __('Status'),
...defaultTableClasses,
+ columnClass: 'gl-w-10p',
},
{
key: 'job',
label: __('Job'),
...defaultTableClasses,
+ columnClass: 'gl-w-20p',
},
{
key: 'pipeline',
label: __('Pipeline'),
...defaultTableClasses,
+ columnClass: 'gl-w-10p',
},
{
key: 'stage',
label: __('Stage'),
...defaultTableClasses,
+ columnClass: 'gl-w-10p',
},
{
key: 'name',
label: __('Name'),
...defaultTableClasses,
+ columnClass: 'gl-w-15p',
},
{
key: 'duration',
label: __('Duration'),
...defaultTableClasses,
+ columnClass: 'gl-w-15p',
},
{
key: 'coverage',
label: __('Coverage'),
- ...defaultTableClasses,
+ tdClass: coverageTdClasses,
+ thClass: defaultTableClasses.thClass,
+ columnClass: 'gl-w-10p',
},
{
key: 'actions',
label: '',
...defaultTableClasses,
+ columnClass: 'gl-w-10p',
},
],
components: {
+ ActionsCell,
+ CiBadge,
+ DurationCell,
GlTable,
+ JobCell,
+ PipelineCell,
},
props: {
jobs: {
@@ -59,9 +83,64 @@ export default {
required: true,
},
},
+ methods: {
+ formatCoverage(coverage) {
+ return coverage ? `${coverage}%` : '';
+ },
+ },
};
</script>
<template>
- <gl-table :items="jobs" :fields="$options.fields" />
+ <gl-table
+ :items="jobs"
+ :fields="$options.fields"
+ :tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ stacked="lg"
+ fixed
+ >
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(status)="{ item }">
+ <ci-badge :status="item.detailedStatus" />
+ </template>
+
+ <template #cell(job)="{ item }">
+ <job-cell :job="item" />
+ </template>
+
+ <template #cell(pipeline)="{ item }">
+ <pipeline-cell :job="item" />
+ </template>
+
+ <template #cell(stage)="{ item }">
+ <div class="gl-text-truncate">
+ <span data-testid="job-stage-name">{{ item.stage.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(name)="{ item }">
+ <div class="gl-text-truncate">
+ <span data-testid="job-name">{{ item.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(duration)="{ item }">
+ <duration-cell :job="item" />
+ </template>
+
+ <template #cell(coverage)="{ item }">
+ <span v-if="item.coverage" data-testid="job-coverage">{{
+ formatCoverage(item.coverage)
+ }}</span>
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <actions-cell :job="item" />
+ </template>
+ </gl-table>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 55954e31654..cf7970f41b1 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -3,6 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
+import JobsTableEmptyState from './jobs_table_empty_state.vue';
import JobsTableTabs from './jobs_table_tabs.vue';
export default {
@@ -13,6 +14,7 @@ export default {
GlAlert,
GlSkeletonLoader,
JobsTable,
+ JobsTableEmptyState,
JobsTableTabs,
},
inject: {
@@ -29,7 +31,7 @@ export default {
};
},
update({ project }) {
- return project?.jobs;
+ return project?.jobs?.nodes || [];
},
error() {
this.hasError = true;
@@ -41,15 +43,21 @@ export default {
jobs: null,
hasError: false,
isAlertDismissed: false,
+ scope: null,
};
},
computed: {
shouldShowAlert() {
return this.hasError && !this.isAlertDismissed;
},
+ showEmptyState() {
+ return this.jobs.length === 0 && !this.scope;
+ },
},
methods: {
fetchJobsByStatus(scope) {
+ this.scope = scope;
+
this.$apollo.queries.jobs.refetch({ statuses: scope });
},
},
@@ -80,6 +88,8 @@ export default {
/>
</div>
- <jobs-table v-else :jobs="jobs.nodes" />
+ <jobs-table-empty-state v-else-if="showEmptyState" />
+
+ <jobs-table v-else :jobs="jobs" />
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue b/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue
new file mode 100644
index 00000000000..fcdd52b719c
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('Jobs|Use jobs to automate your tasks'),
+ description: s__(
+ 'Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.',
+ ),
+ buttonText: s__('Jobs|Create CI/CD configuration file'),
+ },
+ components: {
+ GlEmptyState,
+ },
+ inject: {
+ pipelineEditorPath: {
+ default: '',
+ },
+ emptyStateSvgPath: {
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :description="$options.i18n.description"
+ :svg-path="emptyStateSvgPath"
+ :primary-button-link="pipelineEditorPath"
+ :primary-button-text="$options.i18n.buttonText"
+ />
+</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
index 95d265fce60..26791e4284d 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
@@ -50,7 +50,7 @@ export default {
</script>
<template>
- <gl-tabs>
+ <gl-tabs content-class="gl-pb-0">
<gl-tab
v-for="tab in tabs"
:key="tab.text"
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index d0d625d794d..3040d4e2379 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -22,3 +22,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
primaryText: __('Retry job'),
title: s__('Jobs|Are you sure you want to retry this job?'),
};
+
+export const SUCCESS_STATUS = 'SUCCESS';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 3e00056ee81..260190f5043 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -13,6 +13,7 @@ export default () => {
const {
artifactHelpUrl,
deploymentHelpUrl,
+ codeQualityHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
@@ -38,6 +39,7 @@ export default () => {
props: {
artifactHelpUrl,
deploymentHelpUrl,
+ codeQualityHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
diff --git a/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js b/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js
new file mode 100644
index 00000000000..305d130f10c
--- /dev/null
+++ b/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js
@@ -0,0 +1,10 @@
+import Tracking from '~/tracking';
+
+export default function trackLearnGitlab(learnGitlabA) {
+ Tracking.event('projects:learn_gitlab:index', 'page_init', {
+ label: 'learn_gitlab',
+ property: learnGitlabA
+ ? 'Growth::Conversion::Experiment::LearnGitLabA'
+ : 'Growth::Activation::Experiment::LearnGitLabB',
+ });
+}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index c720476f3bf..cec689a44ca 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -4,6 +4,7 @@ import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { createHttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
+import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
@@ -18,11 +19,21 @@ export const fetchPolicies = {
};
export default (resolvers = {}, config = {}) => {
- let uri = `${gon.relative_url_root || ''}/api/graphql`;
+ const {
+ assumeImmutableResults,
+ baseUrl,
+ batchMax = 10,
+ cacheConfig,
+ fetchPolicy = fetchPolicies.CACHE_FIRST,
+ typeDefs,
+ path = '/api/graphql',
+ useGet = false,
+ } = config;
+ let uri = `${gon.relative_url_root || ''}${path}`;
- if (config.baseUrl) {
+ if (baseUrl) {
// Prepend baseUrl and ensure that `///` are replaced with `/`
- uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/');
+ uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/');
}
const httpOptions = {
@@ -34,7 +45,7 @@ export default (resolvers = {}, config = {}) => {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: 'same-origin',
- batchMax: config.batchMax || 10,
+ batchMax,
};
const requestCounterLink = new ApolloLink((operation, forward) => {
@@ -50,7 +61,7 @@ export default (resolvers = {}, config = {}) => {
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
- config.useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
+ useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
);
const performanceBarLink = new ApolloLink((operation, forward) => {
@@ -73,24 +84,36 @@ export default (resolvers = {}, config = {}) => {
});
});
- return new ApolloClient({
- typeDefs: config.typeDefs,
- link: ApolloLink.from([
+ const hasSubscriptionOperation = ({ query: { definitions } }) => {
+ return definitions.some(
+ ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription',
+ );
+ };
+
+ const appLink = ApolloLink.split(
+ hasSubscriptionOperation,
+ new ActionCableLink(),
+ ApolloLink.from([
requestCounterLink,
performanceBarLink,
new StartupJSLink(),
apolloCaptchaLink,
uploadsLink,
]),
+ );
+
+ return new ApolloClient({
+ typeDefs,
+ link: appLink,
cache: new InMemoryCache({
- ...config.cacheConfig,
- freezeResults: config.assumeImmutableResults,
+ ...cacheConfig,
+ freezeResults: assumeImmutableResults,
}),
resolvers,
- assumeImmutableResults: config.assumeImmutableResults,
+ assumeImmutableResults,
defaultOptions: {
query: {
- fetchPolicy: config.fetchPolicy || fetchPolicies.CACHE_FIRST,
+ fetchPolicy,
},
},
});
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index fb257228597..8666d325c1b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -645,9 +645,6 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) =>
export const convertObjectPropsToSnakeCase = (obj = {}, options = {}) =>
convertObjectProps(convertToSnakeCase, obj, options);
-export const imagePath = (imgUrl) =>
- `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
-
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index a509828815a..0a038febb9f 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -4,6 +4,8 @@ import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js';
import { languageCode, s__, __, n__ } from '../../locale';
+export const SECONDS_IN_DAY = 86400;
+
const DAYS_IN_WEEK = 7;
window.timeago = timeago;
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index 2a8b1759e54..bd47f10b3ac 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,2 +1,3 @@
export const ESC_KEY = 'Escape';
export const ENTER_KEY = 'Enter';
+export const BACKSPACE_KEY = 'Backspace';
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 63feb6f9b1d..e3500d02a79 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -171,3 +171,13 @@ export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-'
export const isNumeric = (value) => {
return !Number.isNaN(parseInt(value, 10));
};
+
+const numberRegex = /^[0-9]+$/;
+
+/**
+ * Checks whether the value is a positive number or 0, or a string with equivalent value
+ *
+ * @param value
+ * @return {boolean}
+ */
+export const isPositiveInteger = (value) => numberRegex.test(value);
diff --git a/app/assets/javascripts/lib/utils/recurrence.js b/app/assets/javascripts/lib/utils/recurrence.js
new file mode 100644
index 00000000000..8fd26f3e393
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/recurrence.js
@@ -0,0 +1,154 @@
+import { uuids } from './uuids';
+
+/**
+ * @module recurrence
+ */
+
+const instances = {};
+
+/**
+ * Create a new unique {@link module:recurrence~RecurInstance|RecurInstance}
+ * @returns {module:recurrence.RecurInstance} The newly created {@link module:recurrence~RecurInstance|RecurInstance}
+ */
+export function create() {
+ const id = uuids()[0];
+ let handlers = {};
+ let count = 0;
+
+ /**
+ * @namespace RecurInstance
+ * @description A RecurInstance tracks the count of any occurrence as registered by calls to <code>occur</code>.
+ * <br /><br />
+ * It maintains an internal counter and a registry of handlers that can be arbitrarily assigned by a user.
+ * <br /><br />
+ * While a RecurInstance isn't specific to any particular use-case, it may be useful for:
+ * <br />
+ * <ul>
+ * <li>Tracking repeated errors across multiple - but not linked - network requests</li>
+ * <li>Tracking repeated user interactions (e.g. multiple clicks)</li>
+ * </ul>
+ * @summary A closure to track repeated occurrences of any arbitrary event.
+ * */
+ const instance = {
+ /**
+ * @type {module:uuids~UUIDv4}
+ * @description A randomly generated {@link module:uuids~UUIDv4|UUID} for this particular recurrence instance
+ * @memberof module:recurrence~RecurInstance
+ * @readonly
+ * @inner
+ */
+ get id() {
+ return id;
+ },
+ /**
+ * @type {Number}
+ * @description The number of times this particular instance of recurrence has been triggered
+ * @memberof module:recurrence~RecurInstance
+ * @readonly
+ * @inner
+ */
+ get count() {
+ return count;
+ },
+ /**
+ * @type {Object}
+ * @description The handlers assigned to this recurrence tracker
+ * @example
+ * myRecurrence.handle( 4, () => console.log( "four" ) );
+ * console.log( myRecurrence.handlers ); // {"4": () => console.log( "four" )}
+ * @memberof module:recurrence~RecurInstance
+ * @readonly
+ * @inner
+ */
+ get handlers() {
+ return handlers;
+ },
+ /**
+ * @type {Boolean}
+ * @description Delete any internal reference to the instance.
+ * <br />
+ * Keep in mind that this will only attempt to remove the <strong>internal</strong> reference.
+ * <br />
+ * If your code maintains a reference to the instance, the regular garbage collector will not free the memory.
+ * @memberof module:recurrence~RecurInstance
+ * @inner
+ */
+ free() {
+ return delete instances[id];
+ },
+ /**
+ * @description Register a handler to be called when this occurrence is seen <code>onCount</code> number of times.
+ * @param {Number} onCount - The number of times the occurrence has been seen to respond to
+ * @param {Function} behavior - A callback function to run when the occurrence has been seen <code>onCount</code> times
+ * @memberof module:recurrence~RecurInstance
+ * @inner
+ */
+ handle(onCount, behavior) {
+ if (onCount && behavior) {
+ handlers[onCount] = behavior;
+ }
+ },
+ /**
+ * @description Remove the behavior callback handler that would be run when the occurrence is seen <code>onCount</code> times
+ * @param {Number} onCount - The count identifier for which to eject the callback handler
+ * @memberof module:recurrence~RecurInstance
+ * @inner
+ */
+ eject(onCount) {
+ if (onCount) {
+ delete handlers[onCount];
+ }
+ },
+ /**
+ * @description Register that this occurrence has been seen and trigger any appropriate handlers
+ * @memberof module:recurrence~RecurInstance
+ * @inner
+ */
+ occur() {
+ count += 1;
+
+ if (typeof handlers[count] === 'function') {
+ handlers[count](count);
+ }
+ },
+ /**
+ * @description Reset this recurrence instance without destroying it entirely
+ * @param {Object} [options]
+ * @param {Boolean} [options.currentCount = true] - Whether to reset the count
+ * @param {Boolean} [options.handlersList = false] - Whether to reset the list of attached handlers back to an empty state
+ * @memberof module:recurrence~RecurInstance
+ * @inner
+ */
+ reset({ currentCount = true, handlersList = false } = {}) {
+ if (currentCount) {
+ count = 0;
+ }
+
+ if (handlersList) {
+ handlers = {};
+ }
+ },
+ };
+
+ instances[id] = instance;
+
+ return instance;
+}
+
+/**
+ * Retrieve a stored {@link module:recurrence~RecurInstance|RecurInstance} by {@link module:uuids~UUIDv4|UUID}
+ * @param {module:uuids~UUIDv4} id - The {@link module:uuids~UUIDv4|UUID} of a previously created {@link module:recurrence~RecurInstance|RecurInstance}
+ * @returns {(module:recurrence~RecurInstance|undefined)} The {@link module:recurrence~RecurInstance|RecurInstance}, or undefined if the UUID doesn't refer to a known Instance
+ */
+export function recall(id) {
+ return instances[id];
+}
+
+/**
+ * Release the memory space for a given {@link module:recurrence~RecurInstance|RecurInstance} by {@link module:uuids~UUIDv4|UUID}
+ * @param {module:uuids~UUIDv4} id - The {@link module:uuids~UUIDv4|UUID} of a previously created {@link module:recurrence~RecurInstance|RecurInstance}
+ * @returns {Boolean} Whether the reference to the stored {@link module:recurrence~RecurInstance|RecurInstance} was released
+ */
+export function free(id) {
+ return recall(id)?.free() || false;
+}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 1593a363dd1..6ff2af47dd8 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -232,7 +232,7 @@ export function insertMarkdownText({
.join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
- textToInsert = tag.replace(textPlaceholder, selected.replace(/\\n/g, '\n'));
+ textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
}
diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/lib/utils/uuids.js
index 98fe4bf9664..98fe4bf9664 100644
--- a/app/assets/javascripts/diffs/utils/uuids.js
+++ b/app/assets/javascripts/lib/utils/uuids.js
diff --git a/app/assets/javascripts/lib/utils/vuex_module_mappers.js b/app/assets/javascripts/lib/utils/vuex_module_mappers.js
new file mode 100644
index 00000000000..95a794dd268
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/vuex_module_mappers.js
@@ -0,0 +1,91 @@
+import { mapValues, isString } from 'lodash';
+import { mapState, mapActions } from 'vuex';
+
+export const REQUIRE_STRING_ERROR_MESSAGE =
+ '`vuex_module_mappers` can only be used with an array of strings, or an object with string values. Consider using the regular `vuex` map helpers instead.';
+
+const normalizeFieldsToObject = (fields) => {
+ return Array.isArray(fields)
+ ? fields.reduce((acc, key) => Object.assign(acc, { [key]: key }), {})
+ : fields;
+};
+
+const mapVuexModuleFields = ({ namespaceSelector, fields, vuexHelper, selector } = {}) => {
+ // The `vuexHelper` needs an object which maps keys to field selector functions.
+ const map = mapValues(normalizeFieldsToObject(fields), (value) => {
+ if (!isString(value)) {
+ throw new Error(REQUIRE_STRING_ERROR_MESSAGE);
+ }
+
+ // We need to use a good ol' function to capture the right "this".
+ return function mappedFieldSelector(...args) {
+ const namespace = namespaceSelector(this);
+
+ return selector(namespace, value, ...args);
+ };
+ });
+
+ return vuexHelper(map);
+};
+
+/**
+ * Like `mapState`, but takes a function in the first param for selecting a namespace.
+ *
+ * ```
+ * computed: {
+ * ...mapVuexModuleState(vm => vm.vuexModule, ['foo']),
+ * }
+ * ```
+ *
+ * @param {Function} namespaceSelector
+ * @param {Array|Object} fields
+ */
+export const mapVuexModuleState = (namespaceSelector, fields) =>
+ mapVuexModuleFields({
+ namespaceSelector,
+ fields,
+ vuexHelper: mapState,
+ selector: (namespace, value, state) => state[namespace][value],
+ });
+
+/**
+ * Like `mapActions`, but takes a function in the first param for selecting a namespace.
+ *
+ * ```
+ * methods: {
+ * ...mapVuexModuleActions(vm => vm.vuexModule, ['fetchFoos']),
+ * }
+ * ```
+ *
+ * @param {Function} namespaceSelector
+ * @param {Array|Object} fields
+ */
+export const mapVuexModuleActions = (namespaceSelector, fields) =>
+ mapVuexModuleFields({
+ namespaceSelector,
+ fields,
+ vuexHelper: mapActions,
+ selector: (namespace, value, dispatch, ...args) => dispatch(`${namespace}/${value}`, ...args),
+ });
+
+/**
+ * Like `mapGetters`, but takes a function in the first param for selecting a namespace.
+ *
+ * ```
+ * computed: {
+ * ...mapGetters(vm => vm.vuexModule, ['hasSearchInfo']),
+ * }
+ * ```
+ *
+ * @param {Function} namespaceSelector
+ * @param {Array|Object} fields
+ */
+export const mapVuexModuleGetters = (namespaceSelector, fields) =>
+ mapVuexModuleFields({
+ namespaceSelector,
+ fields,
+ // `mapGetters` does not let us pass an object which maps to functions. Thankfully `mapState` does
+ // and gives us access to the getters.
+ vuexHelper: mapState,
+ selector: (namespace, value, state, getters) => getters[`${namespace}/${value}`],
+ });
diff --git a/app/assets/javascripts/logs/components/log_advanced_filters.vue b/app/assets/javascripts/logs/components/log_advanced_filters.vue
index 9159ca5b9dc..c6d7c9ad1dc 100644
--- a/app/assets/javascripts/logs/components/log_advanced_filters.vue
+++ b/app/assets/javascripts/logs/components/log_advanced_filters.vue
@@ -1,8 +1,9 @@
<script>
import { GlFilteredSearch } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { timeRanges } from '~/vue_shared/constants';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import TokenWithLoadingState from './tokens/token_with_loading_state.vue';
@@ -54,7 +55,7 @@ export default {
type: TOKEN_TYPE_POD_NAME,
title: s__('Environments|Pod name'),
token: TokenWithLoadingState,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
unique: true,
options: this.podOptions,
loading: this.logs.isLoading,
diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js
index e813f91d2fa..c3dc9f4bc12 100644
--- a/app/assets/javascripts/logs/stores/actions.js
+++ b/app/assets/javascripts/logs/stores/actions.js
@@ -127,7 +127,7 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
return axios
.get(environmentsPath)
.then(({ data }) => {
- commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data.environments);
+ commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data);
dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
})
.catch(() => {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 3f22bd36a4a..6200ade3595 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -35,6 +35,7 @@ import initUsagePingConsent from './usage_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
+import { initTopNav } from './nav';
import 'ee_else_ce/main_ee';
@@ -80,6 +81,7 @@ initRails();
function deferredInitialisation() {
const $body = $('body');
+ initTopNav();
initBreadcrumbs();
initTodoToggle();
initLogoAnimation();
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index cc97d235a9c..cc0533391df 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -1,10 +1,11 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import { setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import { getParameterByName, urlParamsToObject } from '~/lib/utils/common_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { SEARCH_TOKEN_TYPE, SORT_PARAM } from '~/members/constants';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
@@ -17,7 +18,7 @@ export default {
title: s__('Members|2FA'),
token: GlFilteredSearchToken,
unique: true,
- operators: [{ value: '=', description: 'is' }],
+ operators: OPERATOR_IS_ONLY,
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
@@ -30,7 +31,7 @@ export default {
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
- operators: [{ value: '=', description: 'is' }],
+ operators: OPERATOR_IS_ONLY,
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
@@ -63,7 +64,7 @@ export default {
},
},
created() {
- const query = queryToObject(window.location.search);
+ const query = urlParamsToObject(window.location.search);
const tokens = this.tokens
.filter((token) => query[token.type])
@@ -97,9 +98,12 @@ export default {
if (type === SEARCH_TOKEN_TYPE) {
if (value.data !== '') {
+ const { searchParam } = this.filteredSearchBar;
+ const { [searchParam]: searchParamValue } = accumulator;
+
return {
...accumulator,
- [this.filteredSearchBar.searchParam]: value.data,
+ [searchParam]: searchParamValue ? `${searchParamValue} ${value.data}` : value.data,
};
}
} else {
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
new file mode 100644
index 00000000000..37b9135126d
--- /dev/null
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -0,0 +1,124 @@
+<script>
+import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import { MEMBER_TYPES } from '../constants';
+import MembersApp from './app.vue';
+
+const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
+
+export default {
+ name: 'MembersTabs',
+ tabs: [
+ {
+ namespace: MEMBER_TYPES.user,
+ title: __('Members'),
+ },
+ {
+ namespace: MEMBER_TYPES.group,
+ title: __('Groups'),
+ attrs: { 'data-qa-selector': 'groups_list_tab' },
+ },
+ {
+ namespace: MEMBER_TYPES.invite,
+ title: __('Invited'),
+ canManageMembersPermissionsRequired: true,
+ },
+ {
+ namespace: MEMBER_TYPES.accessRequest,
+ title: __('Access requests'),
+ canManageMembersPermissionsRequired: true,
+ },
+ ],
+ urlParams: [],
+ components: { MembersApp, GlTabs, GlTab, GlBadge },
+ inject: ['canManageMembers'],
+ data() {
+ return {
+ selectedTabIndex: 0,
+ };
+ },
+ computed: {
+ ...mapState({
+ userCount(state) {
+ return countComputed(state, MEMBER_TYPES.user);
+ },
+ groupCount(state) {
+ return countComputed(state, MEMBER_TYPES.group);
+ },
+ inviteCount(state) {
+ return countComputed(state, MEMBER_TYPES.invite);
+ },
+ accessRequestCount(state) {
+ return countComputed(state, MEMBER_TYPES.accessRequest);
+ },
+ }),
+ urlParams() {
+ return Object.keys(urlParamsToObject(window.location.search));
+ },
+ activeTabIndexCalculatedFromUrlParams() {
+ return this.$options.tabs.findIndex(({ namespace }) => {
+ return this.getTabUrlParams(namespace).some((urlParam) =>
+ this.urlParams.includes(urlParam),
+ );
+ });
+ },
+ },
+ created() {
+ if (this.activeTabIndexCalculatedFromUrlParams === -1) {
+ return;
+ }
+
+ this.selectedTabIndex = this.activeTabIndexCalculatedFromUrlParams;
+ },
+ methods: {
+ getTabUrlParams(namespace) {
+ const state = this.$store.state[namespace];
+ const urlParams = [];
+
+ if (state?.pagination?.paramName) {
+ urlParams.push(state.pagination.paramName);
+ }
+
+ if (state?.filteredSearchBar?.searchParam) {
+ urlParams.push(state.filteredSearchBar.searchParam);
+ }
+
+ return urlParams;
+ },
+ getTabCount({ namespace }) {
+ return this[`${namespace}Count`];
+ },
+ showTab(tab, index) {
+ if (tab.namespace === MEMBER_TYPES.user) {
+ return true;
+ }
+
+ const { canManageMembersPermissionsRequired = false } = tab;
+ const tabCanBeShown =
+ this.getTabCount(tab) > 0 || this.activeTabIndexCalculatedFromUrlParams === index;
+
+ if (canManageMembersPermissionsRequired) {
+ return this.canManageMembers && tabCanBeShown;
+ }
+
+ return tabCanBeShown;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-tabs v-model="selectedTabIndex">
+ <template v-for="(tab, index) in $options.tabs">
+ <gl-tab v-if="showTab(tab, index)" :key="tab.namespace" :title-link-attributes="tab.attrs">
+ <template slot="title">
+ <span>{{ tab.title }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ getTabCount(tab) }}</gl-badge>
+ </template>
+ <members-app :namespace="tab.namespace" />
+ </gl-tab>
+ </template>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 236aeaef418..09ef98ec411 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -1,8 +1,9 @@
<script>
-import { GlTable, GlBadge } from '@gitlab/ui';
+import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import { FIELDS } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
@@ -19,6 +20,7 @@ export default {
components: {
GlTable,
GlBadge,
+ GlPagination,
MemberAvatar,
CreatedAt,
ExpiresAt,
@@ -43,6 +45,9 @@ export default {
tableAttrs(state) {
return state[this.namespace].tableAttrs;
},
+ pagination(state) {
+ return state[this.namespace].pagination;
+ },
}),
filteredFields() {
return FIELDS.filter(
@@ -59,6 +64,11 @@ export default {
userIsLoggedIn() {
return this.currentUserId !== null;
},
+ showPagination() {
+ const { paramName, currentPage, perPage, totalItems } = this.pagination;
+
+ return paramName && currentPage && perPage && totalItems;
+ },
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
@@ -99,6 +109,11 @@ export default {
...(member?.id && { 'data-testid': `members-table-row-${member.id}` }),
};
},
+ paginationLinkGenerator(page) {
+ const { params = {}, paramName } = this.pagination;
+
+ return mergeUrlParams({ ...params, [paramName]: page }, window.location.href);
+ },
},
};
</script>
@@ -119,6 +134,9 @@ export default {
show-empty
:tbody-tr-attr="tbodyTrAttr"
>
+ <template #head()="{ label }">
+ {{ label }}
+ </template>
<template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar
@@ -179,6 +197,18 @@ export default {
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
</template>
</gl-table>
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :per-page="pagination.perPage"
+ :total-items="pagination.totalItems"
+ :link-gen="paginationLinkGenerator"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :label-next-page="__('Go to next page')"
+ :label-prev-page="__('Go to previous page')"
+ align="center"
+ />
<remove-group-link-modal />
<ldap-override-confirmation-modal />
</div>
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 6376b3fa75a..6c913af8a0f 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
-import { parseDataAttributes } from 'ee_else_ce/members/utils';
+import { parseDataAttributes } from '~/members/utils';
import App from './components/app.vue';
import membersStore from './store';
diff --git a/app/assets/javascripts/members/store/state.js b/app/assets/javascripts/members/store/state.js
index 4006b4b501d..5415b1c5f25 100644
--- a/app/assets/javascripts/members/store/state.js
+++ b/app/assets/javascripts/members/store/state.js
@@ -1,5 +1,6 @@
export default ({
members,
+ pagination,
tableFields,
tableAttrs,
tableSortableFields,
@@ -8,6 +9,7 @@ export default ({
filteredSearchBar,
}) => ({
members,
+ pagination,
tableFields,
tableAttrs,
tableSortableFields,
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 2bf30dd7b6e..be549b40885 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,9 +1,5 @@
import { isUndefined } from 'lodash';
-import {
- getParameterByName,
- convertObjectPropsToCamelCase,
- parseBoolean,
-} from '~/lib/utils/common_utils';
+import { getParameterByName, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
@@ -105,14 +101,12 @@ export const buildSortHref = ({
export const canOverride = () => false;
export const parseDataAttributes = (el) => {
- const { members, sourceId, memberPath, canManageMembers } = el.dataset;
+ const { membersData } = el.dataset;
- return {
- members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
- sourceId: parseInt(sourceId, 10),
- memberPath,
- canManageMembers: parseBoolean(canManageMembers),
- };
+ return convertObjectPropsToCamelCase(JSON.parse(membersData), {
+ deep: true,
+ ignoreKeyNames: ['params'],
+ });
};
export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
index 7649c363daa..04e493712ec 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
import { deprecatedCreateFlash as flash } from '~/flash';
@@ -7,6 +8,9 @@ import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE } from '../constants';
export default {
+ components: {
+ GlButton,
+ },
props: {
file: {
type: Object,
@@ -100,21 +104,21 @@ export default {
};
</script>
<template>
- <div v-show="file.showEditor" class="diff-editor-wrap">
- <div v-if="file.promptDiscardConfirmation" class="discard-changes-alert-wrap">
- <div class="discard-changes-alert">
- {{ __('Are you sure you want to discard your changes?') }}
- <div class="discard-actions">
- <button
- class="btn btn-sm btn-danger-secondary gl-button"
- @click="acceptDiscardConfirmation(file)"
- >
- {{ __('Discard changes') }}
- </button>
- <button class="btn btn-default btn-sm gl-button" @click="cancelDiscardConfirmation(file)">
- {{ __('Cancel') }}
- </button>
- </div>
+ <div v-show="file.showEditor">
+ <div v-if="file.promptDiscardConfirmation" class="discard-changes-alert">
+ {{ __('Are you sure you want to discard your changes?') }}
+ <div class="gl-ml-3 gl-display-inline-block">
+ <gl-button
+ size="small"
+ variant="danger"
+ category="secondary"
+ @click="acceptDiscardConfirmation(file)"
+ >
+ {{ __('Discard changes') }}
+ </gl-button>
+ <gl-button size="small" @click="cancelDiscardConfirmation(file)">
+ {{ __('Cancel') }}
+ </gl-button>
</div>
</div>
<div :class="classObject" class="editor-wrap">
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
index 9721481e6be..a856d38c089 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
@@ -35,7 +35,7 @@ export default {
<td :class="lineCssClass(line)" class="diff-line-num header"></td>
<td :class="lineCssClass(line)" class="line_content header">
<strong>{{ line.richText }}</strong>
- <button class="btn" @click="handleSelected({ file, line })">
+ <button type="button" @click="handleSelected({ file, line })">
{{ line.buttonTitle }}
</button>
</td>
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
index 7b1d947ccff..2c89b614c87 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
@@ -35,7 +35,7 @@ export default {
<td class="diff-line-num header" :class="lineCssClass(line)"></td>
<td class="line_content header" :class="lineCssClass(line)">
<strong>{{ line.richText }}</strong>
- <button class="btn" @click="handleSelected({ file, line })">
+ <button type="button" @click="handleSelected({ file, line })">
{{ line.buttonTitle }}
</button>
</td>
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
index 0509cf0afa1..3e31e2e93ae 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton, GlButtonGroup } from '@gitlab/ui';
import { mapGetters, mapState, mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -20,6 +20,8 @@ import { INTERACTIVE_RESOLVE_MODE } from './constants';
*/
export default {
components: {
+ GlButton,
+ GlButtonGroup,
GlSprintf,
FileIcon,
DiffFileEditor,
@@ -77,28 +79,23 @@ export default {
{{ conflictsData.errorMessage }}
</div>
<template v-if="!isLoading && !hasError">
- <div class="content-block oneline-block files-changed">
- <div v-if="fileTextTypePresent" class="inline-parallel-buttons">
- <div class="btn-group">
- <button
- :class="{ active: !isParallel }"
- class="btn gl-button"
- @click="setViewType('inline')"
- >
+ <div class="gl-border-b-0 gl-py-5 gl-line-height-32">
+ <div v-if="fileTextTypePresent" class="gl-float-right">
+ <gl-button-group>
+ <gl-button :selected="!isParallel" @click="setViewType('inline')">
{{ __('Inline') }}
- </button>
- <button
- :class="{ active: isParallel }"
- class="btn gl-button"
+ </gl-button>
+ <gl-button
+ :selected="isParallel"
data-testid="side-by-side"
@click="setViewType('parallel')"
>
{{ __('Side-by-side') }}
- </button>
- </div>
+ </gl-button>
+ </gl-button-group>
</div>
<div class="js-toggle-container">
- <div class="commit-stat-summary" data-testid="conflicts-count">
+ <div data-testid="conflicts-count">
<gl-sprintf :message="$options.i18n.commitStatSummary">
<template #conflict>
<strong class="cred">{{ getConflictsCountText }}</strong>
@@ -127,47 +124,43 @@ export default {
<strong class="file-title-name">{{ file.filePath }}</strong>
</div>
<div class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start">
- <div v-if="file.type === 'text'" class="btn-group gl-mr-3">
- <button
- :class="{ active: file.resolveMode === 'interactive' }"
- class="btn gl-button"
- type="button"
+ <gl-button-group v-if="file.type === 'text'" class="gl-mr-3">
+ <gl-button
+ :selected="file.resolveMode === 'interactive'"
data-testid="interactive-button"
@click="onClickResolveModeButton(file, 'interactive')"
>
{{ __('Interactive mode') }}
- </button>
- <button
- :class="{ active: file.resolveMode === 'edit' }"
- class="btn gl-button"
- type="button"
+ </gl-button>
+ <gl-button
+ :selected="file.resolveMode === 'edit'"
data-testid="inline-button"
@click="onClickResolveModeButton(file, 'edit')"
>
{{ __('Edit inline') }}
- </button>
- </div>
- <a :href="file.blobPath" class="btn gl-button view-file">
+ </gl-button>
+ </gl-button-group>
+ <gl-button :href="file.blobPath">
<gl-sprintf :message="__('View file @ %{commitSha}')">
<template #commitSha>
{{ conflictsData.shortCommitSha }}
</template>
</gl-sprintf>
- </a>
+ </gl-button>
</div>
</div>
<div class="diff-content diff-wrap-lines">
- <template v-if="file.resolveMode === 'interactive' && file.type === 'text'">
- <div v-if="!isParallel" class="file-content">
- <inline-conflict-lines :file="file" />
- </div>
- <div v-if="isParallel" class="file-content">
- <parallel-conflict-lines :file="file" />
- </div>
- </template>
- <div v-if="file.resolveMode === 'edit' || file.type === 'text-editor'">
- <diff-file-editor :file="file" />
+ <div
+ v-if="file.resolveMode === 'interactive' && file.type === 'text'"
+ class="file-content"
+ >
+ <parallel-conflict-lines v-if="isParallel" :file="file" />
+ <inline-conflict-lines v-else :file="file" />
</div>
+ <diff-file-editor
+ v-if="file.resolveMode === 'edit' || file.type === 'text-editor'"
+ :file="file"
+ />
</div>
</div>
</div>
@@ -176,10 +169,10 @@ export default {
<div class="resolve-conflicts-form">
<div class="form-group row">
<div class="col-md-4">
- <h4>
+ <h4 class="gl-mt-0">
{{ __('Resolve conflicts on source branch') }}
</h4>
- <div class="resolve-info">
+ <div class="gl-mb-5" data-testid="resolve-info">
<gl-sprintf :message="$options.i18n.resolveInfo">
<template #use_ours>
<code>{{ s__('MergeConflict|Use ours') }}</code>
@@ -199,7 +192,7 @@ export default {
<label class="label-bold" for="commit-message">
{{ __('Commit message') }}
</label>
- <div class="commit-message-container">
+ <div class="commit-message-container gl-mb-4">
<div class="max-width-marker"></div>
<textarea
id="commit-message"
@@ -209,27 +202,17 @@ export default {
rows="5"
></textarea>
</div>
- </div>
- </div>
- <div class="form-group row">
- <div class="offset-md-4 col-md-8">
- <div class="row">
- <div class="col-6">
- <button
- :disabled="!isReadyToCommit"
- class="btn gl-button btn-success js-submit-button"
- type="button"
- @click="submitResolvedConflicts(resolveConflictsPath)"
- >
- <span>{{ getCommitButtonText }}</span>
- </button>
- </div>
- <div class="col-6 text-right">
- <a :href="mergeRequestPath" class="gl-button btn btn-default">
- {{ __('Cancel') }}
- </a>
- </div>
- </div>
+ <gl-button
+ :disabled="!isReadyToCommit"
+ variant="confirm"
+ class="js-submit-button"
+ @click="submitResolvedConflicts(resolveConflictsPath)"
+ >
+ {{ getCommitButtonText }}
+ </gl-button>
+ <gl-button :href="mergeRequestPath">
+ {{ __('Cancel') }}
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue
deleted file mode 100644
index 526aafc1def..00000000000
--- a/app/assets/javascripts/merge_request/components/status_box.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-import mrEventHub from '../eventhub';
-
-const CLASSES = {
- opened: 'status-box-open',
- locked: 'status-box-open',
- closed: 'status-box-mr-closed',
- merged: 'status-box-mr-merged',
-};
-
-const STATUS = {
- opened: [__('Open'), 'issue-open-m'],
- locked: [__('Open'), 'issue-open-m'],
- closed: [__('Closed'), 'issue-close'],
- merged: [__('Merged'), 'git-merge'],
-};
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- initialState: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- state: this.initialState,
- };
- },
- computed: {
- statusBoxClass() {
- return CLASSES[this.state];
- },
- statusHumanName() {
- return STATUS[this.state][0];
- },
- statusIconName() {
- return STATUS[this.state][1];
- },
- },
- created() {
- mrEventHub.$on('mr.state.updated', this.updateState);
- },
- beforeDestroy() {
- mrEventHub.$off('mr.state.updated', this.updateState);
- },
- methods: {
- updateState({ state }) {
- this.state = state;
- },
- },
-};
-</script>
-
-<template>
- <div :class="statusBoxClass" class="issuable-status-box status-box">
- <gl-icon
- :name="statusIconName"
- class="gl-display-block gl-sm-display-none!"
- data-testid="status-icon"
- />
- <span class="gl-display-none gl-sm-display-block">
- {{ statusHumanName }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/merge_request/eventhub.js b/app/assets/javascripts/merge_request/eventhub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/merge_request/eventhub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 67b24793a65..d5db9f43d09 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -355,6 +355,8 @@ export default class MergeRequestTabs {
this.commitPipelinesTable = new Vue({
provide: {
+ artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
+ artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
},
render(createElement) {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index bfb18206b62..05e7fb7a3e9 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -8,6 +8,7 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import { defaultTimeRange } from '~/vue_shared/constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { metricStates, keyboardShortcutKeys } from '../constants';
@@ -28,6 +29,7 @@ import VariablesSection from './variables_section.vue';
export default {
components: {
+ AlertsDeprecationWarning,
VueDraggable,
DashboardHeader,
DashboardPanel,
@@ -394,6 +396,8 @@ export default {
<template>
<div class="prometheus-graphs" data-qa-selector="prometheus_graphs">
+ <alerts-deprecation-warning />
+
<dashboard-header
v-if="showHeader"
ref="prometheusGraphsHeader"
diff --git a/app/assets/javascripts/monitoring/monitoring_app.js b/app/assets/javascripts/monitoring/monitoring_app.js
index ee67e5dd827..cf79e71b9e0 100644
--- a/app/assets/javascripts/monitoring/monitoring_app.js
+++ b/app/assets/javascripts/monitoring/monitoring_app.js
@@ -12,7 +12,10 @@ export default (props = {}) => {
if (el && el.dataset) {
const { metricsDashboardBasePath, ...dataset } = el.dataset;
- const { initState, dataProps } = stateAndPropsFromDataset(dataset);
+ const {
+ initState,
+ dataProps: { hasManagedPrometheus, ...dataProps },
+ } = stateAndPropsFromDataset(dataset);
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
@@ -21,6 +24,7 @@ export default (props = {}) => {
el,
store,
router,
+ provide: { hasManagedPrometheus },
data() {
return {
dashboardProps: { ...dataProps, ...props },
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 6306415a8b9..8adf1862af2 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -41,6 +41,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
+ dataProps.hasManagedPrometheus = parseBoolean(dataProps.hasManagedPrometheus);
return {
initState: {
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
new file mode 100644
index 00000000000..f8f3ba26536
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlNav, GlNavItemDropdown, GlDropdownForm, GlTooltip } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
+
+const TOOLTIP = s__('TopNav|Switch to...');
+
+export default {
+ components: {
+ GlNav,
+ GlNavItemDropdown,
+ GlDropdownForm,
+ GlTooltip,
+ TopNavDropdownMenu,
+ },
+ props: {
+ navData: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ findTooltipTarget() {
+ // ### Why use a target function instead of `v-gl-tooltip`?
+ // To get the tooltip to align correctly, we need it to target the actual
+ // toggle button which we don't directly render.
+ return this.$el.querySelector('.js-top-nav-dropdown-toggle');
+ },
+ },
+ TOOLTIP,
+};
+</script>
+
+<template>
+ <gl-nav class="navbar-sub-nav">
+ <gl-nav-item-dropdown
+ :text="navData.activeTitle"
+ icon="dot-grid"
+ menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!"
+ toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
+ no-flip
+ >
+ <gl-dropdown-form>
+ <top-nav-dropdown-menu
+ :primary="navData.primary"
+ :secondary="navData.secondary"
+ :views="navData.views"
+ />
+ </gl-dropdown-form>
+ </gl-nav-item-dropdown>
+ <gl-tooltip
+ boundary="window"
+ :boundary-padding="0"
+ :target="findTooltipTarget"
+ placement="right"
+ :title="$options.TOOLTIP"
+ />
+ </gl-nav>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
new file mode 100644
index 00000000000..21ff3ebcd7d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_container_view.vue
@@ -0,0 +1,74 @@
+<script>
+import FrequentItemsApp from '~/frequent_items/components/app.vue';
+import eventHub from '~/frequent_items/event_hub';
+import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+export default {
+ components: {
+ FrequentItemsApp,
+ TopNavMenuItem,
+ VuexModuleProvider,
+ },
+ props: {
+ frequentItemsVuexModule: {
+ type: String,
+ required: true,
+ },
+ frequentItemsDropdownType: {
+ type: String,
+ required: true,
+ },
+ linksPrimary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ linksSecondary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ linkGroups() {
+ return [
+ { key: 'primary', links: this.linksPrimary },
+ { key: 'secondary', links: this.linksSecondary },
+ ].filter((x) => x.links?.length);
+ },
+ },
+ mounted() {
+ // For historic reasons, the frequent-items-app component requires this too start up.
+ this.$nextTick(() => {
+ eventHub.$emit(`${this.frequentItemsDropdownType}-dropdownOpen`);
+ });
+ },
+};
+</script>
+
+<template>
+ <div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
+ <div class="frequent-items-dropdown-container gl-w-auto">
+ <div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
+ <vuex-module-provider :vuex-module="frequentItemsVuexModule">
+ <frequent-items-app v-bind="$attrs" />
+ </vuex-module-provider>
+ </div>
+ </div>
+ <div
+ v-for="({ key, links }, groupIndex) in linkGroups"
+ :key="key"
+ :class="{ 'gl-mt-3': groupIndex !== 0 }"
+ class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
+ data-testid="menu-item-group"
+ >
+ <top-nav-menu-item
+ v-for="(link, linkIndex) in links"
+ :key="link.title"
+ :menu-item="link"
+ :class="{ 'gl-mt-1': linkIndex !== 0 }"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
new file mode 100644
index 00000000000..1cbd64b501d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
@@ -0,0 +1,144 @@
+<script>
+import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
+import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
+import TopNavContainerView from './top_nav_container_view.vue';
+import TopNavMenuItem from './top_nav_menu_item.vue';
+
+const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
+const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
+
+export default {
+ components: {
+ KeepAliveSlots,
+ TopNavContainerView,
+ TopNavMenuItem,
+ },
+ props: {
+ primary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ secondary: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ views: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ activeId: '',
+ };
+ },
+ computed: {
+ menuItemGroups() {
+ return [
+ { key: 'primary', items: this.primary, classes: '' },
+ {
+ key: 'secondary',
+ items: this.secondary,
+ classes: SECONDARY_GROUP_CLASS,
+ },
+ ].filter((x) => x.items?.length);
+ },
+ allMenuItems() {
+ return this.menuItemGroups.flatMap((x) => x.items);
+ },
+ activeMenuItem() {
+ return this.allMenuItems.find((x) => x.id === this.activeId);
+ },
+ activeView() {
+ return this.activeMenuItem?.view;
+ },
+ menuClass() {
+ if (!this.activeView) {
+ return 'gl-w-full';
+ }
+
+ return '';
+ },
+ },
+ created() {
+ // Initialize activeId based on initialization prop
+ this.activeId = this.allMenuItems.find((x) => x.active)?.id;
+ },
+ methods: {
+ onClick({ id, href }) {
+ // If we're a link, let's just do the default behavior so the view won't change
+ if (href) {
+ return;
+ }
+
+ this.activeId = id;
+ },
+ menuItemClasses(menuItem) {
+ if (menuItem.id === this.activeId) {
+ return ACTIVE_CLASS;
+ }
+
+ return '';
+ },
+ },
+ FREQUENT_ITEMS_PROJECTS,
+ FREQUENT_ITEMS_GROUPS,
+ // expose for unit tests
+ ACTIVE_CLASS,
+ SECONDARY_GROUP_CLASS,
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-stretch">
+ <div
+ class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10"
+ :class="menuClass"
+ data-testid="menu-sidebar"
+ >
+ <div
+ class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
+ >
+ <div
+ v-for="group in menuItemGroups"
+ :key="group.key"
+ :class="group.classes"
+ data-testid="menu-item-group"
+ >
+ <top-nav-menu-item
+ v-for="(menu, index) in group.items"
+ :key="menu.id"
+ data-testid="menu-item"
+ :class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
+ :menu-item="menu"
+ @click="onClick(menu)"
+ />
+ </div>
+ </div>
+ </div>
+ <keep-alive-slots
+ v-show="activeView"
+ :slot-key="activeView"
+ class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
+ data-testid="menu-subview"
+ >
+ <template #projects>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
+ v-bind="views.projects"
+ />
+ </template>
+ <template #groups>
+ <top-nav-container-view
+ :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
+ :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
+ v-bind="views.groups"
+ />
+ </template>
+ </keep-alive-slots>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
new file mode 100644
index 00000000000..a0d92811a6f
--- /dev/null
+++ b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ },
+ props: {
+ menuItem: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ category="tertiary"
+ :href="menuItem.href"
+ class="top-nav-menu-item gl-display-block"
+ v-on="$listeners"
+ >
+ <span class="gl-display-flex">
+ <gl-icon v-if="menuItem.icon" :name="menuItem.icon" class="gl-mr-2!" />
+ {{ menuItem.title }}
+ <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
+ </span>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js
new file mode 100644
index 00000000000..646ce3f0ecf
--- /dev/null
+++ b/app/assets/javascripts/nav/index.js
@@ -0,0 +1,12 @@
+export const initTopNav = async () => {
+ const el = document.getElementById('js-top-nav');
+
+ if (!el) {
+ return;
+ }
+
+ // With combined_menu feature flag, there's a benefit to splitting up the import
+ const { mountTopNav } = await import(/* webpackChunkName: 'top_nav' */ './mount');
+
+ mountTopNav(el);
+};
diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js
new file mode 100644
index 00000000000..0d46ff56249
--- /dev/null
+++ b/app/assets/javascripts/nav/mount.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import App from './components/top_nav_app.vue';
+import { createStore } from './stores';
+
+Vue.use(Vuex);
+
+export const mountTopNav = (el) => {
+ const viewModel = JSON.parse(el.dataset.viewModel);
+ const store = createStore();
+
+ return new Vue({
+ el,
+ store,
+ render(h) {
+ return h(App, {
+ props: {
+ navData: viewModel,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/nav/stores/index.js b/app/assets/javascripts/nav/stores/index.js
new file mode 100644
index 00000000000..527bbdd5c3f
--- /dev/null
+++ b/app/assets/javascripts/nav/stores/index.js
@@ -0,0 +1,4 @@
+import Vuex from 'vuex';
+import { createStoreOptions } from '~/frequent_items/store';
+
+export const createStore = () => new Vuex.Store(createStoreOptions());
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index c09db6851e5..9bf26e5a182 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -3,6 +3,7 @@
import katex from 'katex';
import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
+import { hasContent } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
@@ -88,6 +89,38 @@ renderer.listitem = (t) => {
const [text, inline] = renderKatex(t);
return `<li class="${inline ? 'inline-katex' : ''}">${text}</li>`;
};
+renderer.originalImage = renderer.image;
+
+renderer.image = function image(href, title, text) {
+ const attachmentHeader = `attachment:`; // eslint-disable-line @gitlab/require-i18n-strings
+
+ if (!this.attachments || !href.startsWith(attachmentHeader)) {
+ return this.originalImage(href, title, text);
+ }
+
+ let img = ``;
+ const filename = href.substring(attachmentHeader.length);
+
+ if (hasContent(filename)) {
+ const attachment = this.attachments[filename];
+
+ if (attachment) {
+ const imageType = Object.keys(attachment)[0];
+
+ if (hasContent(imageType)) {
+ const data = attachment[imageType];
+ const inlined = `data:${imageType};base64,${data}"`; // eslint-disable-line @gitlab/require-i18n-strings
+ img = this.originalImage(inlined, title, text);
+ }
+ }
+ }
+
+ if (!hasContent(img)) {
+ return this.originalImage(href, title, text);
+ }
+
+ return sanitize(img);
+};
marked.setOptions({
renderer,
@@ -105,6 +138,8 @@ export default {
},
computed: {
markdown() {
+ renderer.attachments = this.cell.attachments;
+
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b5c59f34e87..c324c846f47 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1732,7 +1732,7 @@ export default class Notes {
// Submission failed, revert back to original note
$noteBodyText.html(escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in');
- $editingNote.find('.spinner').remove();
+ $editingNote.find('.gl-spinner').remove();
// Show Flash message about failure
this.updateNoteError();
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 79d8ce78329..90be5b3e470 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -15,6 +15,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
@@ -162,7 +163,7 @@ export default {
canToggleIssueState() {
return (
this.getNoteableData.current_user.can_update &&
- this.getNoteableData.state !== constants.MERGED &&
+ this.openState !== constants.MERGED &&
!this.closedAndLocked
);
},
@@ -283,6 +284,7 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
+ .then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() => Flash(constants.toggleStateErrorMessage[this.noteableType][this.openState]));
},
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 24399e669a6..0cc818c6d0e 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -278,7 +278,6 @@ export default {
v-if="canResolve"
ref="resolveButton"
v-gl-tooltip
- size="small"
category="tertiary"
:variant="resolveVariant"
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
@@ -292,7 +291,7 @@ export default {
<template v-if="canAwardEmoji">
<emoji-picker
v-if="glFeatures.improvedEmojiPicker"
- toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-2 gl-p-0! gl-shadow-none! gl-bg-transparent!"
+ toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
@click="setAwardEmoji"
>
<template #button-content>
@@ -305,10 +304,9 @@ export default {
v-else
v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
- class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
+ class="note-action-button note-emoji-button add-reaction-button btn-icon js-add-award js-note-emoji"
category="tertiary"
variant="default"
- size="small"
:title="$options.i18n.addReactionLabel"
:aria-label="$options.i18n.addReactionLabel"
data-position="right"
@@ -336,7 +334,6 @@ export default {
:title="$options.i18n.editCommentLabel"
:aria-label="$options.i18n.editCommentLabel"
icon="pencil"
- size="small"
category="tertiary"
class="note-action-button js-note-edit"
data-qa-selector="note_edit_button"
@@ -347,24 +344,24 @@ export default {
v-gl-tooltip
:title="$options.i18n.deleteCommentLabel"
:aria-label="$options.i18n.deleteCommentLabel"
- size="small"
icon="remove"
category="tertiary"
class="note-action-button js-note-delete"
@click="onDelete"
/>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
+ <!-- eslint-disable @gitlab/vue-no-data-toggle -->
<gl-button
v-gl-tooltip
:title="$options.i18n.moreActionsLabel"
:aria-label="$options.i18n.moreActionsLabel"
icon="ellipsis_v"
- size="small"
category="tertiary"
class="note-action-button more-actions-toggle"
data-toggle="dropdown"
@click="closeTooltip"
/>
+ <!-- eslint-enable @gitlab/vue-no-data-toggle -->
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<gl-dropdown-item v-if="canReportAsAbuse" :href="reportAbusePath">
{{ __('Report abuse to admin') }}
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index 5ce03091504..0cd2afcf8a0 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -22,7 +22,6 @@ export default {
data-track-event="click_button"
data-track-label="reply_comment_button"
category="tertiary"
- size="small"
icon="comment"
:title="$options.i18n.buttonText"
:aria-label="$options.i18n.buttonText"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index a70bac94b71..4ce81219f11 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -362,36 +362,26 @@ export default {
</template>
</markdown-field>
</comment-field-layout>
- <div class="note-form-actions clearfix">
+ <div class="note-form-actions">
<template v-if="showBatchCommentsActions">
<p v-if="showResolveDiscussionToggle">
<label>
<template v-if="discussionResolved">
- <input
- v-model="isUnresolving"
- type="checkbox"
- class="js-unresolve-checkbox"
- data-qa-selector="unresolve_review_discussion_checkbox"
- />
+ <input v-model="isUnresolving" type="checkbox" class="js-unresolve-checkbox" />
{{ __('Unresolve thread') }}
</template>
<template v-else>
- <input
- v-model="isResolving"
- type="checkbox"
- class="js-resolve-checkbox"
- data-qa-selector="resolve_review_discussion_checkbox"
- />
+ <input v-model="isResolving" type="checkbox" class="js-resolve-checkbox" />
{{ __('Resolve thread') }}
</template>
</label>
</p>
- <div class="gl-display-sm-flex gl-flex-wrap">
+ <div class="gl-display-flex gl-flex-wrap gl-mb-n3">
<gl-button
:disabled="isDisabled"
category="primary"
variant="confirm"
- class="gl-mr-3"
+ class="gl-sm-mr-3 gl-mb-3"
data-qa-selector="start_review_button"
@click="handleAddToReview"
>
@@ -401,15 +391,15 @@ export default {
<gl-button
:disabled="isDisabled"
category="secondary"
- variant="default"
+ variant="confirm"
data-qa-selector="comment_now_button"
- class="gl-mr-3 js-comment-button"
+ class="gl-sm-mr-3 gl-mb-3 js-comment-button"
@click="handleUpdate()"
>
{{ __('Add comment now') }}
</gl-button>
<gl-button
- class="note-edit-cancel js-close-discussion-note-form"
+ class="note-edit-cancel gl-mb-3 js-close-discussion-note-form"
category="secondary"
variant="default"
data-testid="cancelBatchCommentsEnabled"
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1204d68159f..bdb85360be8 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -345,7 +345,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
// this is a temporary solution until we have confidentiality real-time updates
if (
confidentialWidget.setConfidentiality &&
- message.some((m) => m.includes('confidential'))
+ message.some((m) => m.includes('Made this issue confidential'))
) {
confidentialWidget.setConfidentiality();
}
@@ -468,15 +468,6 @@ const getFetchDataParams = (state) => {
return { endpoint, options };
};
-export const fetchData = ({ commit, state, getters, dispatch }) => {
- const { endpoint, options } = getFetchDataParams(state);
-
- axios
- .get(endpoint, options)
- .then(({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch))
- .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
-};
-
export const poll = ({ commit, state, getters, dispatch }) => {
eTagPoll = new Poll({
resource: {
@@ -493,7 +484,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
if (!Visibility.hidden()) {
eTagPoll.makeDelayedRequest(2500);
} else {
- dispatch('fetchData');
+ eTagPoll.makeRequest();
}
Visibility.change(() => {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 39f66063cfb..b04b1d28ffa 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,4 +1,6 @@
import { flattenDeep, clone } from 'lodash';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
+import { isInMRPage } from '~/lib/utils/common_utils';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -82,7 +84,8 @@ export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issue
export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note);
-export const openState = (state) => state.noteableData.state;
+export const openState = (state) =>
+ isInMRPage() ? statusBoxState.state : state.noteableData.state;
export const getUserData = (state) => state.userData || {};
diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
index b9532cb7e72..6974de99344 100644
--- a/app/assets/javascripts/packages/details/components/maven_installation.vue
+++ b/app/assets/javascripts/packages/details/components/maven_installation.vue
@@ -28,10 +28,15 @@ export default {
'mavenSetupXml',
'gradleGroovyInstalCommand',
'gradleGroovyAddSourceCommand',
+ 'gradleKotlinInstalCommand',
+ 'gradleKotlinAddSourceCommand',
]),
showMaven() {
return this.instructionType === 'maven';
},
+ showGroovy() {
+ return this.instructionType === 'groovy';
+ },
},
i18n: {
xmlText: s__(
@@ -47,8 +52,9 @@ export default {
trackingActions: { ...TrackingActions },
TrackingLabels,
installOptions: [
- { value: 'maven', label: s__('PackageRegistry|Show Maven commands') },
- { value: 'groovy', label: s__('PackageRegistry|Show Gradle Groovy DSL commands') },
+ { value: 'maven', label: s__('PackageRegistry|Maven XML') },
+ { value: 'groovy', label: s__('PackageRegistry|Gradle Groovy DSL') },
+ { value: 'kotlin', label: s__('PackageRegistry|Gradle Kotlin DSL') },
],
};
</script>
@@ -107,7 +113,7 @@ export default {
</template>
</gl-sprintf>
</template>
- <template v-else>
+ <template v-else-if="showGroovy">
<code-instruction
class="gl-mb-5"
:label="s__('PackageRegistry|Gradle Groovy DSL install command')"
@@ -125,5 +131,23 @@ export default {
multiline
/>
</template>
+ <template v-else>
+ <code-instruction
+ class="gl-mb-5"
+ :label="s__('PackageRegistry|Gradle Kotlin DSL install command')"
+ :instruction="gradleKotlinInstalCommand"
+ :copy-text="s__('PackageRegistry|Copy Gradle Kotlin DSL install command')"
+ :tracking-action="$options.trackingActions.COPY_KOTLIN_INSTALL_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ />
+ <code-instruction
+ :label="s__('PackageRegistry|Add Gradle Kotlin DSL repository command')"
+ :instruction="gradleKotlinAddSourceCommand"
+ :copy-text="s__('PackageRegistry|Copy add Gradle Kotlin DSL repository command')"
+ :tracking-action="$options.trackingActions.COPY_KOTLIN_ADD_TO_SOURCE_COMMAND"
+ :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION"
+ multiline
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue
index 18f15e2c63e..6b0fcf5e4fe 100644
--- a/app/assets/javascripts/packages/details/components/npm_installation.vue
+++ b/app/assets/javascripts/packages/details/components/npm_installation.vue
@@ -14,6 +14,11 @@ export default {
GlLink,
GlSprintf,
},
+ data() {
+ return {
+ instructionType: 'npm',
+ };
+ },
computed: {
...mapState(['npmHelpPath']),
...mapGetters(['npmInstallationCommand', 'npmSetupCommand']),
@@ -29,6 +34,9 @@ export default {
yarnSetupCommand() {
return this.npmSetupCommand(NpmManager.YARN);
},
+ showNpm() {
+ return this.instructionType === 'npm';
+ },
},
i18n: {
helpText: s__(
@@ -37,16 +45,23 @@ export default {
},
trackingActions: { ...TrackingActions },
TrackingLabels,
- installOptions: [{ value: 'npm', label: s__('PackageRegistry|Show NPM commands') }],
+ installOptions: [
+ { value: 'npm', label: s__('PackageRegistry|Show NPM commands') },
+ { value: 'yarn', label: s__('PackageRegistry|Show Yarn commands') },
+ ],
};
</script>
<template>
<div>
- <installation-title package-type="npm" :options="$options.installOptions" />
+ <installation-title
+ package-type="npm"
+ :options="$options.installOptions"
+ @change="instructionType = $event"
+ />
<code-instruction
- :label="s__('PackageRegistry|npm command')"
+ v-if="showNpm"
:instruction="npmCommand"
:copy-text="s__('PackageRegistry|Copy npm command')"
:tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
@@ -54,7 +69,7 @@ export default {
/>
<code-instruction
- :label="s__('PackageRegistry|yarn command')"
+ v-else
:instruction="yarnCommand"
:copy-text="s__('PackageRegistry|Copy yarn command')"
:tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
@@ -64,7 +79,7 @@ export default {
<h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
<code-instruction
- :label="s__('PackageRegistry|npm command')"
+ v-if="showNpm"
:instruction="npmSetup"
:copy-text="s__('PackageRegistry|Copy npm setup command')"
:tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
@@ -72,7 +87,7 @@ export default {
/>
<code-instruction
- :label="s__('PackageRegistry|yarn command')"
+ v-else
:instruction="yarnSetupCommand"
:copy-text="s__('PackageRegistry|Copy yarn setup command')"
:tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
diff --git a/app/assets/javascripts/packages/details/components/package_files.vue b/app/assets/javascripts/packages/details/components/package_files.vue
index c5e929fe2a4..103d1f489bd 100644
--- a/app/assets/javascripts/packages/details/components/package_files.vue
+++ b/app/assets/javascripts/packages/details/components/package_files.vue
@@ -92,6 +92,7 @@ export default {
<template #cell(commit)="{ item }">
<gl-link
+ v-if="item.pipeline && item.pipeline.project"
:href="item.pipeline.project.commit_url"
class="gl-text-gray-500"
data-testid="commit-link"
diff --git a/app/assets/javascripts/packages/details/constants.js b/app/assets/javascripts/packages/details/constants.js
index f0300ee29b4..cd34b1ad45a 100644
--- a/app/assets/javascripts/packages/details/constants.js
+++ b/app/assets/javascripts/packages/details/constants.js
@@ -38,6 +38,9 @@ export const TrackingActions = {
COPY_GRADLE_INSTALL_COMMAND: 'copy_gradle_install_command',
COPY_GRADLE_ADD_TO_SOURCE_COMMAND: 'copy_gradle_add_to_source_command',
+
+ COPY_KOTLIN_INSTALL_COMMAND: 'copy_kotlin_install_command',
+ COPY_KOTLIN_ADD_TO_SOURCE_COMMAND: 'copy_kotlin_add_to_source_command',
};
export const NpmManager = {
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
index fb9b7d61fd2..ae273e26d6a 100644
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -126,4 +126,15 @@ export const gradleGroovyAddSourceCommand = ({ mavenPath }) =>
url '${mavenPath}'
}`;
+export const gradleKotlinInstalCommand = ({ packageEntity }) => {
+ const {
+ app_group: group = '',
+ app_name: name = '',
+ app_version: version = '',
+ } = packageEntity.maven_metadatum;
+ return `implementation("${group}:${name}:${version}")`;
+};
+
+export const gradleKotlinAddSourceCommand = ({ mavenPath }) => `maven("${mavenPath}")`;
+
export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0;
diff --git a/app/assets/javascripts/packages/list/components/package_search.vue b/app/assets/javascripts/packages/list/components/package_search.vue
index 2e183b1b978..869a2c2f641 100644
--- a/app/assets/javascripts/packages/list/components/package_search.vue
+++ b/app/assets/javascripts/packages/list/components/package_search.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { sortableFields } from '../utils';
@@ -14,7 +15,7 @@ export default {
title: s__('PackageRegistry|Type'),
unique: true,
token: PackageTypeToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
},
],
components: { RegistrySearch, UrlSync },
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index b4fe3c70dea..d871c2e4d24 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -93,3 +93,5 @@ export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry');
export const LIST_INTRO_TEXT = s__(
'PackageRegistry|Publish and share packages for a variety of common package managers. %{docLinkStart}More information%{docLinkEnd}',
);
+
+export const TERRAFORM_SEARCH_TYPE = Object.freeze({ value: { data: 'terraform_module' } });
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
index 195117b9ddb..8dfe3c82ab3 100644
--- a/app/assets/javascripts/packages/list/stores/actions.js
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -8,6 +8,7 @@ import {
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
MISSING_DELETE_PATH_ERROR,
+ TERRAFORM_SEARCH_TYPE,
} from '../constants';
import { getNewPaginationPage } from '../utils';
import * as types from './mutation_types';
@@ -27,8 +28,9 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
const { sort, orderBy } = state.sorting;
-
- const type = state.filter.find((f) => f.type === 'type');
+ const type = state.config.forceTerraform
+ ? TERRAFORM_SEARCH_TYPE
+ : state.filter.find((f) => f.type === 'type');
const name = state.filter.find((f) => f.type === 'filtered-search-term');
const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data };
diff --git a/app/assets/javascripts/packages/list/stores/mutations.js b/app/assets/javascripts/packages/list/stores/mutations.js
index 4ce13cfcb29..98165e581b0 100644
--- a/app/assets/javascripts/packages/list/stores/mutations.js
+++ b/app/assets/javascripts/packages/list/stores/mutations.js
@@ -4,9 +4,8 @@ import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, config) {
- const { comingSoonJson, ...rest } = config;
state.config = {
- ...rest,
+ ...config,
isGroupPage: config.pageType === GROUP_PAGE_TYPE,
};
},
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index 4de4c191e51..eee0e470c7b 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -1,7 +1,9 @@
<script>
import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import { s__ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { PACKAGE_ERROR_STATUS, PACKAGE_DEFAULT_STATUS } from '../constants';
import { getPackageTypeLabel } from '../utils';
import PackagePath from './package_path.vue';
import PackageTags from './package_tags.vue';
@@ -70,22 +72,45 @@ export default {
hasProjectLink() {
return Boolean(this.packageEntity.project_path);
},
+ showWarningIcon() {
+ return this.packageEntity.status === PACKAGE_ERROR_STATUS;
+ },
+ disabledRow() {
+ return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
+ },
+ disabledDeleteButton() {
+ return this.disabledRow || !this.packageEntity._links.delete_api_path;
+ },
+ },
+ i18n: {
+ erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
},
};
</script>
<template>
- <list-item data-qa-selector="package_row">
+ <list-item data-qa-selector="package_row" :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link
:href="packageLink"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
+ :disabled="disabledRow"
>
<gl-truncate :text="packageEntity.name" />
</gl-link>
+ <gl-button
+ v-if="showWarningIcon"
+ v-gl-tooltip="{ title: $options.i18n.erroredPackageText }"
+ class="gl-hover-bg-transparent!"
+ icon="warning"
+ category="tertiary"
+ data-testid="warning-icon"
+ :aria-label="__('Warning')"
+ />
+
<package-tags
v-if="packageEntity.tags && packageEntity.tags.length"
class="gl-ml-3"
@@ -109,7 +134,11 @@ export default {
{{ packageType }}
</component>
- <package-path v-if="hasProjectLink" :path="packageEntity.project_path" />
+ <package-path
+ v-if="hasProjectLink"
+ :path="packageEntity.project_path"
+ :disabled="disabledRow"
+ />
</div>
</template>
@@ -137,7 +166,7 @@ export default {
variant="danger"
:title="s__('PackageRegistry|Remove package')"
:aria-label="s__('PackageRegistry|Remove package')"
- :disabled="!packageEntity._links.delete_api_path"
+ :disabled="disabledDeleteButton"
@click="$emit('packageToDelete', packageEntity)"
/>
</template>
diff --git a/app/assets/javascripts/packages/shared/components/package_path.vue b/app/assets/javascripts/packages/shared/components/package_path.vue
index 9afe06ab497..6fb001e5e92 100644
--- a/app/assets/javascripts/packages/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages/shared/components/package_path.vue
@@ -16,6 +16,11 @@ export default {
type: String,
required: true,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
pathPieces() {
@@ -45,7 +50,12 @@ export default {
<div data-qa-selector="package-path" class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
- <gl-link data-testid="root-link" class="gl-text-gray-500 gl-min-w-0" :href="`/${rootLink}`">
+ <gl-link
+ data-testid="root-link"
+ class="gl-text-gray-500 gl-min-w-0"
+ :href="`/${rootLink}`"
+ :disabled="disabled"
+ >
{{ root }}
</gl-link>
@@ -63,7 +73,12 @@ export default {
<gl-icon data-testid="ellipsis-chevron" name="chevron-right" class="gl-mx-2 gl-min-w-0" />
</template>
- <gl-link data-testid="leaf-link" class="gl-text-gray-500 gl-min-w-0" :href="`/${path}`">
+ <gl-link
+ data-testid="leaf-link"
+ class="gl-text-gray-500 gl-min-w-0"
+ :href="`/${path}`"
+ :disabled="disabled"
+ >
{{ leaf }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index f7de31c2c86..b3df542e0ae 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -26,3 +26,8 @@ export const TrackingCategories = {
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
+
+export const PACKAGE_ERROR_STATUS = 'error';
+export const PACKAGE_DEFAULT_STATUS = 'default';
+export const PACKAGE_HIDDEN_STATUS = 'hidden';
+export const PACKAGE_PROCESSING_STATUS = 'processing';
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
index 88ee8a4200e..7e6e98f4fb5 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js
@@ -9,7 +9,7 @@ Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
const store = createStore();
- store.dispatch('setInitialState', el.dataset);
+ store.dispatch('setInitialState', { ...el.dataset, forceTerraform: true });
return new Vue({
el,
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
new file mode 100644
index 00000000000..d66a30e7e81
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
@@ -0,0 +1,118 @@
+<script>
+import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+
+import {
+ DUPLICATES_TOGGLE_LABEL,
+ DUPLICATES_ALLOWED_DISABLED,
+ DUPLICATES_ALLOWED_ENABLED,
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+} from '~/packages_and_registries/settings/group/constants';
+
+export default {
+ name: 'DuplicatesSettings',
+ i18n: {
+ DUPLICATES_TOGGLE_LABEL,
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+ },
+ components: {
+ GlSprintf,
+ GlToggle,
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ duplicatesAllowed: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ duplicateExceptionRegex: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ duplicateExceptionRegexError: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ modelNames: {
+ type: Object,
+ required: true,
+ validator(value) {
+ return isEqual(Object.keys(value), ['allowed', 'exception']);
+ },
+ },
+ toggleQaSelector: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ labelQaSelector: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ enabledButtonLabel() {
+ return this.duplicatesAllowed ? DUPLICATES_ALLOWED_ENABLED : DUPLICATES_ALLOWED_DISABLED;
+ },
+ isExceptionRegexValid() {
+ return !this.duplicateExceptionRegexError;
+ },
+ },
+ methods: {
+ update(type, value) {
+ this.$emit('update', { [type]: value });
+ },
+ },
+};
+</script>
+
+<template>
+ <form>
+ <div class="gl-display-flex">
+ <gl-toggle
+ :data-qa-selector="toggleQaSelector"
+ :label="$options.i18n.DUPLICATES_TOGGLE_LABEL"
+ label-position="hidden"
+ :value="duplicatesAllowed"
+ @change="update(modelNames.allowed, $event)"
+ />
+ <div class="gl-ml-5">
+ <div data-testid="toggle-label" :data-qa-selector="labelQaSelector">
+ <gl-sprintf :message="enabledButtonLabel">
+ <template #bold="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </div>
+ <gl-form-group
+ v-if="!duplicatesAllowed"
+ class="gl-mt-4"
+ :label="$options.i18n.DUPLICATES_SETTING_EXCEPTION_TITLE"
+ label-size="sm"
+ :state="isExceptionRegexValid"
+ :invalid-feedback="duplicateExceptionRegexError"
+ :description="$options.i18n.DUPLICATES_SETTINGS_EXCEPTION_LEGEND"
+ label-for="maven-duplicated-settings-regex-input"
+ >
+ <gl-form-input
+ id="maven-duplicated-settings-regex-input"
+ :value="duplicateExceptionRegex"
+ @change="update(modelNames.exception, $event)"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue
new file mode 100644
index 00000000000..e5f63fe8d0d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue
@@ -0,0 +1,26 @@
+<script>
+import { s__ } from '~/locale';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
+
+export default {
+ name: 'GenericSettings',
+ components: {
+ SettingsTitles,
+ },
+ i18n: {
+ title: s__('PackageRegistry|Generic'),
+ subTitle: s__('PackageRegistry|Settings for Generic packages'),
+ },
+ modelNames: {
+ allowed: 'genericDuplicatesAllowed',
+ exception: 'genericDuplicateExceptionRegex',
+ },
+};
+</script>
+
+<template>
+ <div>
+ <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" />
+ <slot :model-names="$options.modelNames"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 4f5c53ed4a3..01d4861f5c2 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -1,7 +1,8 @@
<script>
import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
+import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
-
import {
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
@@ -30,6 +31,8 @@ export default {
GlLink,
SettingsBlock,
MavenSettings,
+ GenericSettings,
+ DuplicatesSettings,
},
inject: ['defaultExpanded', 'groupPath'],
apollo: {
@@ -128,13 +131,32 @@ export default {
</span>
</template>
<template #default>
- <maven-settings
- :maven-duplicates-allowed="packageSettings.mavenDuplicatesAllowed"
- :maven-duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex"
- :maven-duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex"
- :loading="isLoading"
- @update="updateSettings"
- />
+ <maven-settings data-testid="maven-settings">
+ <template #default="{ modelNames }">
+ <duplicates-settings
+ :duplicates-allowed="packageSettings.mavenDuplicatesAllowed"
+ :duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex"
+ :duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex"
+ :model-names="modelNames"
+ :loading="isLoading"
+ toggle-qa-selector="allow_duplicates_toggle"
+ label-qa-selector="allow_duplicates_label"
+ @update="updateSettings"
+ />
+ </template>
+ </maven-settings>
+ <generic-settings class="gl-mt-6" data-testid="generic-settings">
+ <template #default="{ modelNames }">
+ <duplicates-settings
+ :duplicates-allowed="packageSettings.genericDuplicatesAllowed"
+ :duplicate-exception-regex="packageSettings.genericDuplicateExceptionRegex"
+ :duplicate-exception-regex-error="errors.genericDuplicateExceptionRegex"
+ :model-names="modelNames"
+ :loading="isLoading"
+ @update="updateSettings"
+ />
+ </template>
+ </generic-settings>
</template>
</settings-block>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
index faacabb44ce..a1cbd695f34 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
@@ -1,118 +1,26 @@
<script>
-import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
-
-import {
- MAVEN_TOGGLE_LABEL,
- MAVEN_TITLE,
- MAVEN_SETTINGS_SUBTITLE,
- MAVEN_DUPLICATES_ALLOWED_DISABLED,
- MAVEN_DUPLICATES_ALLOWED_ENABLED,
- MAVEN_SETTING_EXCEPTION_TITLE,
- MAVEN_SETTINGS_EXCEPTION_LEGEND,
- MAVEN_DUPLICATES_ALLOWED,
- MAVEN_DUPLICATE_EXCEPTION_REGEX,
-} from '~/packages_and_registries/settings/group/constants';
+import { s__ } from '~/locale';
+import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
export default {
name: 'MavenSettings',
- i18n: {
- MAVEN_TOGGLE_LABEL,
- MAVEN_TITLE,
- MAVEN_SETTINGS_SUBTITLE,
- MAVEN_SETTING_EXCEPTION_TITLE,
- MAVEN_SETTINGS_EXCEPTION_LEGEND,
- },
- modelNames: {
- MAVEN_DUPLICATES_ALLOWED,
- MAVEN_DUPLICATE_EXCEPTION_REGEX,
- },
components: {
- GlSprintf,
- GlToggle,
- GlFormGroup,
- GlFormInput,
- },
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- mavenDuplicatesAllowed: {
- type: Boolean,
- default: false,
- required: true,
- },
- mavenDuplicateExceptionRegex: {
- type: String,
- default: '',
- required: true,
- },
- mavenDuplicateExceptionRegexError: {
- type: String,
- default: '',
- required: false,
- },
+ SettingsTitles,
},
- computed: {
- enabledButtonLabel() {
- return this.mavenDuplicatesAllowed
- ? MAVEN_DUPLICATES_ALLOWED_ENABLED
- : MAVEN_DUPLICATES_ALLOWED_DISABLED;
- },
- isMavenDuplicateExceptionRegexValid() {
- return !this.mavenDuplicateExceptionRegexError;
- },
+ i18n: {
+ title: s__('PackageRegistry|Maven'),
+ subTitle: s__('PackageRegistry|Settings for Maven packages'),
},
- methods: {
- update(type, value) {
- this.$emit('update', { [type]: value });
- },
+ modelNames: {
+ allowed: 'mavenDuplicatesAllowed',
+ exception: 'mavenDuplicateExceptionRegex',
},
};
</script>
<template>
<div>
- <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200">
- {{ $options.i18n.MAVEN_TITLE }}
- </h5>
- <p>{{ $options.i18n.MAVEN_SETTINGS_SUBTITLE }}</p>
- <form>
- <div class="gl-display-flex">
- <gl-toggle
- data-qa-selector="allow_duplicates_toggle"
- :label="$options.i18n.MAVEN_TOGGLE_LABEL"
- label-position="hidden"
- :value="mavenDuplicatesAllowed"
- @change="update($options.modelNames.MAVEN_DUPLICATES_ALLOWED, $event)"
- />
- <div class="gl-ml-5">
- <div data-testid="toggle-label" data-qa-selector="allow_duplicates_label">
- <gl-sprintf :message="enabledButtonLabel">
- <template #bold="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </div>
- <gl-form-group
- v-if="!mavenDuplicatesAllowed"
- class="gl-mt-4"
- :label="$options.i18n.MAVEN_SETTING_EXCEPTION_TITLE"
- label-size="sm"
- :state="isMavenDuplicateExceptionRegexValid"
- :invalid-feedback="mavenDuplicateExceptionRegexError"
- :description="$options.i18n.MAVEN_SETTINGS_EXCEPTION_LEGEND"
- label-for="maven-duplicated-settings-regex-input"
- >
- <gl-form-input
- id="maven-duplicated-settings-regex-input"
- :value="mavenDuplicateExceptionRegex"
- @change="update($options.modelNames.MAVEN_DUPLICATE_EXCEPTION_REGEX, $event)"
- />
- </gl-form-group>
- </div>
- </div>
- </form>
+ <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" />
+ <slot :model-names="$options.modelNames"></slot>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
new file mode 100644
index 00000000000..3f0ab7686e5
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
@@ -0,0 +1,25 @@
+<script>
+export default {
+ name: 'SettingsTitle',
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ subTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200">
+ {{ title }}
+ </h5>
+ <p>{{ subTitle }}</p>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index d52a6a626f9..a2256c5c371 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -6,17 +6,15 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__(
'PackageRegistry|GitLab Packages allows organizations to utilize GitLab as a private repository for a variety of common package formats. %{linkStart}More Information%{linkEnd}',
);
-export const MAVEN_TITLE = s__('PackageRegistry|Maven');
-export const MAVEN_SETTINGS_SUBTITLE = s__('PackageRegistry|Settings for Maven packages');
-export const MAVEN_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
-export const MAVEN_DUPLICATES_ALLOWED_DISABLED = s__(
+export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
+export const DUPLICATES_ALLOWED_DISABLED = s__(
'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.',
);
-export const MAVEN_DUPLICATES_ALLOWED_ENABLED = s__(
+export const DUPLICATES_ALLOWED_ENABLED = s__(
'PackageRegistry|%{boldStart}Allow duplicates%{boldEnd} - Packages with the same name and version are accepted.',
);
-export const MAVEN_SETTING_EXCEPTION_TITLE = __('Exceptions');
-export const MAVEN_SETTINGS_EXCEPTION_LEGEND = s__(
+export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
+export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
'PackageRegistry|Packages can be published if their name or version matches this regex',
);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
index 1fc59bd3496..5c245ff9453 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
@@ -3,6 +3,8 @@ mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsIn
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
+ genericDuplicatesAllowed
+ genericDuplicateExceptionRegex
}
errors
}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
index 2011659887d..a1c01300893 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -3,6 +3,8 @@ query getGroupPackagesSettings($fullPath: ID!) {
packageSettings {
mavenDuplicatesAllowed
mavenDuplicateExceptionRegex
+ genericDuplicatesAllowed
+ genericDuplicateExceptionRegex
}
}
}
diff --git a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
index d75fb31fd98..d75fb31fd98 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue
diff --git a/app/assets/javascripts/registry/settings/components/expiration_input.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
index 42b7c7918a5..d6d85189792 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_input.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue
@@ -1,6 +1,9 @@
<script>
import { GlFormGroup, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
-import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants';
+import {
+ NAME_REGEX_LENGTH,
+ TEXT_AREA_INVALID_FEEDBACK,
+} from '~/packages_and_registries/settings/project/constants';
export default {
components: {
diff --git a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue
index fd9ca6a54c5..0c595fa79b4 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue
@@ -1,6 +1,9 @@
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
+import {
+ NEXT_CLEANUP_LABEL,
+ NOT_SCHEDULED_POLICY_TEXT,
+} from '~/packages_and_registries/settings/project/constants';
export default {
components: {
diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue
index 6aa78c69ba9..7a9ea7c0bf7 100644
--- a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue
@@ -1,7 +1,10 @@
<script>
import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import { ENABLED_TOGGLE_DESCRIPTION, DISABLED_TOGGLE_DESCRIPTION } from '../constants';
+import {
+ ENABLED_TOGGLE_DESCRIPTION,
+ DISABLED_TOGGLE_DESCRIPTION,
+} from '~/packages_and_registries/settings/project/constants';
export default {
i18n: {
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 480590ec71e..edbe9441e57 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -7,8 +7,8 @@ import {
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
UNAVAILABLE_ADMIN_FEATURE_TEXT,
-} from '../constants';
-import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
+} from '~/packages_and_registries/settings/project/constants';
+import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from './settings_form.vue';
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
index 1360e09a75d..41be70a3ad5 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
@@ -17,10 +17,10 @@ import {
NAME_REGEX_DESCRIPTION,
CADENCE_LABEL,
EXPIRATION_POLICY_FOOTER_NOTE,
-} from '~/registry/settings/constants';
-import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
-import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
-import { formOptionsGenerator } from '~/registry/settings/utils';
+} from '~/packages_and_registries/settings/project/constants';
+import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql';
+import { updateContainerExpirationPolicy } from '~/packages_and_registries/settings/project/graphql/utils/cache_update';
+import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils';
import Tracking from '~/tracking';
import ExpirationDropdown from './expiration_dropdown.vue';
import ExpirationInput from './expiration_input.vue';
diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 165c4aae3cb..165c4aae3cb 100644
--- a/app/assets/javascripts/registry/settings/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
diff --git a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql
index 1d6c89133af..1d6c89133af 100644
--- a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql
diff --git a/app/assets/javascripts/registry/settings/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
index 16152eb81f6..16152eb81f6 100644
--- a/app/assets/javascripts/registry/settings/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js
diff --git a/app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql
index c40cd115ab0..c40cd115ab0 100644
--- a/app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql
diff --git a/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql
index c171be0ad07..c171be0ad07 100644
--- a/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql
diff --git a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js
index c4b2af13862..c4b2af13862 100644
--- a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
index 65af6f846aa..65af6f846aa 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
diff --git a/app/assets/javascripts/registry/settings/utils.js b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
index 4a2d7c7d466..4a2d7c7d466 100644
--- a/app/assets/javascripts/registry/settings/utils.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index cf06ee2c22a..d6fa1be29b0 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,3 +1,5 @@
-import initDevOpsScoreEmptyState from '~/analytics/devops_report/devops_score_empty_state';
+import initDevOpsScore from '~/analytics/devops_report/devops_score';
+import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
-initDevOpsScoreEmptyState();
+initDevOpsScoreDisabledUsagePing();
+initDevOpsScore();
diff --git a/app/assets/javascripts/pages/admin/labels/index/index.js b/app/assets/javascripts/pages/admin/labels/index/index.js
index e5ab5d43bbf..17ee7c03ed6 100644
--- a/app/assets/javascripts/pages/admin/labels/index/index.js
+++ b/app/assets/javascripts/pages/admin/labels/index/index.js
@@ -1,3 +1,21 @@
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
+document.addEventListener('DOMContentLoaded', () => {
+ const pagination = document.querySelector('.labels .gl-pagination');
+ const emptyState = document.querySelector('.labels .nothing-here-block.hidden');
-document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
+ function removeLabelSuccessCallback() {
+ this.closest('li').classList.add('gl-display-none!');
+
+ const labelsCount = document.querySelectorAll(
+ 'ul.manage-labels-list li:not(.gl-display-none\\!)',
+ ).length;
+
+ // display the empty state if there are no more labels
+ if (labelsCount < 1 && !pagination && emptyState) {
+ emptyState.classList.remove('hidden');
+ }
+ }
+
+ document.querySelectorAll('.js-remove-label').forEach((row) => {
+ row.addEventListener('ajax:success', removeLabelSuccessCallback);
+ });
+});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 20407334b3f..a3b78da6ef5 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,6 +1,8 @@
<script>
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { s__, sprintf } from '~/locale';
+import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
export default {
components: {
@@ -8,6 +10,7 @@ export default {
GlButton,
GlFormInput,
GlSprintf,
+ OncallSchedulesList,
},
props: {
title: {
@@ -42,6 +45,11 @@ export default {
type: String,
required: true,
},
+ oncallSchedules: {
+ type: String,
+ required: false,
+ default: '[]',
+ },
},
data() {
return {
@@ -58,6 +66,14 @@ export default {
canSubmit() {
return this.enteredUsername === this.username;
},
+ schedules() {
+ try {
+ return JSON.parse(this.oncallSchedules);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+ return [];
+ },
},
methods: {
show() {
@@ -96,6 +112,8 @@ export default {
</gl-sprintf>
</p>
+ <oncall-schedules-list v-if="schedules.length" :schedules="schedules" />
+
<p>
<gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
<template #username>
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index b1079c3b068..9a8b0c9990f 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
-import { initAdminUsersApp, initCohortsEmptyState } from '~/admin/users';
-import initTabs from '~/admin/users/tabs';
+import { initAdminUsersApp } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
import csrf from '~/lib/utils/csrf';
import Translate from '~/vue_shared/translate';
@@ -62,6 +61,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
initConfirmModal();
- initCohortsEmptyState();
- initTabs();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index b60607e8857..76db578f6f9 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
-import initIssuablesList from '~/issues_list';
+import { mountIssuablesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
@@ -12,8 +12,6 @@ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
-initIssuablesList();
-
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
@@ -22,3 +20,7 @@ initFilteredSearch({
});
projectSelect();
initManualOrdering();
+
+if (gon.features?.vueIssuablesList) {
+ mountIssuablesListApp();
+}
diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js
index af0264c7992..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/groups/milestones/edit/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js
index af0264c7992..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/groups/milestones/new/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
-initForm(false);
+initForm();
diff --git a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js b/app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
index 3b922622d2c..3b922622d2c 100644
--- a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
+++ b/app/assets/javascripts/pages/groups/settings/packages_and_registries/show/index.js
diff --git a/app/assets/javascripts/pages/groups/settings/repository/show/index.js b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
index 92405f205cb..f048955dadf 100644
--- a/app/assets/javascripts/pages/groups/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
@@ -1,7 +1,8 @@
-import DueDateSelectors from '~/due_date_select';
+import initDatePicker from '~/behaviors/date_picker';
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
-new DueDateSelectors(); // eslint-disable-line no-new
+// Used for deploy tokens "expires at" field
+initDatePicker();
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 9e75985c130..2aec0617b5a 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -3,6 +3,7 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
@@ -24,4 +25,5 @@ export default function initGroupDetails(actionName = 'show') {
new ProjectsList();
initInviteMembersBanner();
+ initInviteMembersModal();
}
diff --git a/app/assets/javascripts/pages/help/show/index.js b/app/assets/javascripts/pages/help/show/index.js
deleted file mode 100644
index ec426a850b6..00000000000
--- a/app/assets/javascripts/pages/help/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initHelp from '~/help/help';
-
-document.addEventListener('DOMContentLoaded', initHelp);
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index fc2702b8c37..8a8ce70e998 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,25 +1,35 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import BlobViewer from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
+import createDefaultClient from '~/lib/graphql';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
const viewBlobEl = document.querySelector('#js-view-blob-app');
if (viewBlobEl) {
- const { blobPath } = viewBlobEl.dataset;
+ const { blobPath, projectPath } = viewBlobEl.dataset;
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
+ apolloProvider,
render(createElement) {
return createElement(BlobContentViewer, {
props: {
path: blobPath,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 90a663802d2..d75c3cc6b8b 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -33,7 +33,7 @@ if (filesContainer.length) {
axios
.get(batchPath)
.then(({ data }) => {
- filesContainer.html($(data.html));
+ filesContainer.html($(data));
syntaxHighlight(filesContainer);
handleLocationHash();
new Diff();
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
deleted file mode 100644
index 768da8fb236..00000000000
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initCompareAutocomplete from '~/compare_autocomplete';
-
-document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 288d6711682..07cc0ce46bc 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -20,6 +20,7 @@ import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import validation from '~/vue_shared/directives/validation';
const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
@@ -31,6 +32,13 @@ const ALLOWED_VISIBILITY = {
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
};
+const initFormField = ({ value, required = true, skipValidation = false }) => ({
+ value,
+ required,
+ state: skipValidation ? true : null,
+ feedback: null,
+});
+
export default {
components: {
GlForm,
@@ -46,6 +54,9 @@ export default {
GlFormRadioGroup,
GlFormSelect,
},
+ directives: {
+ validation: validation(),
+ },
inject: {
newGroupPath: {
default: '',
@@ -77,7 +88,8 @@ export default {
},
projectDescription: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
projectVisibility: {
type: String,
@@ -85,16 +97,30 @@ export default {
},
},
data() {
+ const form = {
+ state: false,
+ showValidation: false,
+ fields: {
+ namespace: initFormField({
+ value: null,
+ }),
+ name: initFormField({ value: this.projectName }),
+ slug: initFormField({ value: this.projectPath }),
+ description: initFormField({
+ value: this.projectDescription,
+ required: false,
+ skipValidation: true,
+ }),
+ visibility: initFormField({
+ value: this.projectVisibility,
+ skipValidation: true,
+ }),
+ },
+ };
return {
isSaving: false,
namespaces: [],
- selectedNamespace: {},
- fork: {
- name: this.projectName,
- slug: this.projectPath,
- description: this.projectDescription,
- visibility: this.projectVisibility,
- },
+ form,
};
},
computed: {
@@ -106,7 +132,7 @@ export default {
},
namespaceAllowedVisibility() {
return (
- ALLOWED_VISIBILITY[this.selectedNamespace.visibility] ||
+ ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
);
},
@@ -139,16 +165,17 @@ export default {
},
},
watch: {
- selectedNamespace(newVal) {
+ // eslint-disable-next-line func-names
+ 'form.fields.namespace.value': function (newVal) {
const { visibility } = newVal;
if (this.projectAllowedVisibility.includes(visibility)) {
- this.fork.visibility = visibility;
+ this.form.fields.visibility.value = visibility;
}
},
// eslint-disable-next-line func-names
- 'fork.name': function (newVal) {
- this.fork.slug = kebabCase(newVal);
+ 'form.fields.name.value': function (newVal) {
+ this.form.fields.slug.value = kebabCase(newVal);
},
},
mounted() {
@@ -166,19 +193,25 @@ export default {
);
},
async onSubmit() {
+ this.form.showValidation = true;
+
+ if (!this.form.state) {
+ return;
+ }
+
this.isSaving = true;
+ this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility } = this.fork;
- const { id: namespaceId } = this.selectedNamespace;
+ const { name, slug, description, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
- name,
- namespace_id: namespaceId,
- path: slug,
- description,
- visibility,
+ name: name.value,
+ namespace_id: namespace.value.id,
+ path: slug.value,
+ description: description.value,
+ visibility: visibility.value,
};
const forkProjectPath = `/api/:version/projects/:id/fork`;
@@ -198,16 +231,34 @@ export default {
</script>
<template>
- <gl-form method="POST" @submit.prevent="onSubmit">
+ <gl-form novalidate method="POST" @submit.prevent="onSubmit">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <gl-form-group label="Project name" label-for="fork-name">
- <gl-form-input id="fork-name" v-model="fork.name" data-testid="fork-name-input" required />
+ <gl-form-group
+ :label="__('Project name')"
+ label-for="fork-name"
+ :invalid-feedback="form.fields.name.feedback"
+ >
+ <gl-form-input
+ id="fork-name"
+ v-model="form.fields.name.value"
+ v-validation:[form.showValidation]
+ name="name"
+ data-testid="fork-name-input"
+ :state="form.fields.name.state"
+ required
+ />
</gl-form-group>
<div class="gl-md-display-flex">
<div class="gl-flex-basis-half">
- <gl-form-group label="Project URL" label-for="fork-url" class="gl-md-mr-3">
+ <gl-form-group
+ :label="__('Project URL')"
+ label-for="fork-url"
+ class="gl-md-mr-3"
+ :state="form.fields.namespace.state"
+ :invalid-feedback="s__('ForkProject|Please select a namespace')"
+ >
<gl-form-input-group>
<template #prepend>
<gl-input-group-text>
@@ -216,9 +267,12 @@ export default {
</template>
<gl-form-select
id="fork-url"
- v-model="selectedNamespace"
+ v-model="form.fields.namespace.value"
+ v-validation:[form.showValidation]
+ name="namespace"
data-testid="fork-url-input"
data-qa-selector="fork_namespace_dropdown"
+ :state="form.fields.namespace.state"
required
>
<template slot="first">
@@ -232,11 +286,19 @@ export default {
</gl-form-group>
</div>
<div class="gl-flex-basis-half">
- <gl-form-group label="Project slug" label-for="fork-slug" class="gl-md-ml-3">
+ <gl-form-group
+ :label="__('Project slug')"
+ label-for="fork-slug"
+ class="gl-md-ml-3"
+ :invalid-feedback="form.fields.slug.feedback"
+ >
<gl-form-input
id="fork-slug"
- v-model="fork.slug"
+ v-model="form.fields.slug.value"
+ v-validation:[form.showValidation]
data-testid="fork-slug-input"
+ name="slug"
+ :state="form.fields.slug.state"
required
/>
</gl-form-group>
@@ -250,11 +312,13 @@ export default {
</gl-link>
</p>
- <gl-form-group label="Project description (optional)" label-for="fork-description">
+ <gl-form-group :label="__('Project description (optional)')" label-for="fork-description">
<gl-form-textarea
id="fork-description"
- v-model="fork.description"
+ v-model="form.fields.description.value"
data-testid="fork-description-textarea"
+ name="description"
+ :state="form.fields.description.state"
/>
</gl-form-group>
@@ -266,8 +330,9 @@ export default {
</gl-link>
</label>
<gl-form-radio-group
- v-model="fork.visibility"
+ v-model="form.fields.visibility.value"
data-testid="fork-visibility-radio-group"
+ name="visibility"
required
>
<gl-form-radio
@@ -291,6 +356,7 @@ export default {
type="submit"
category="primary"
variant="confirm"
+ class="js-no-auto-disable"
data-testid="submit-button"
data-qa-selector="fork_project_button"
:loading="isSaving"
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 85489ae8687..8cd703133f5 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,36 +1,38 @@
-/* eslint-disable no-new */
-
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initCsvImportExportButtons from '~/issuable/init_csv_import_export_buttons';
import initIssuableByEmail from '~/issuable/init_issuable_by_email';
import IssuableIndex from '~/issuable_index';
-import initIssuablesList, { initIssuesListApp } from '~/issues_list';
+import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
-IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
-
-initFilteredSearch({
- page: FILTERED_SEARCH.ISSUES,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
- useDefaultState: true,
-});
-
if (gon.features?.vueIssuesList) {
- new IssuableIndex();
+ mountIssuesListApp();
} else {
- new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+
+ initFilteredSearch({
+ page: FILTERED_SEARCH.ISSUES,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
+ });
+
+ new IssuableIndex(ISSUABLE_INDEX.ISSUE); // eslint-disable-line no-new
+ new UsersSelect(); // eslint-disable-line no-new
+
+ initCsvImportExportButtons();
+ initIssuableByEmail();
+ initManualOrdering();
+
+ if (gon.features?.vueIssuablesList) {
+ mountIssuablesListApp();
+ }
}
-new ShortcutsNavigation();
-new UsersSelect();
+new ShortcutsNavigation(); // eslint-disable-line no-new
-initManualOrdering();
-initIssuablesList();
-initIssuableByEmail();
-initCsvImportExportButtons();
-initIssuesListApp();
+mountJiraIssuesListApp();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 5be9f6117dc..d906c579697 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,4 +1,4 @@
-import initIssuablesList from '~/issues_list';
+import { mountIssuablesListApp } from '~/issues_list';
import FilteredSearchServiceDesk from './filtered_search';
const supportBotData = JSON.parse(
@@ -11,5 +11,5 @@ if (document.querySelector('.filtered-search')) {
}
if (gon.features?.vueIssuablesList) {
- initIssuablesList();
+ mountIssuablesListApp();
}
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 2b679a83eac..3143ff5adac 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -1,8 +1,6 @@
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
@@ -58,7 +56,5 @@ export default function initShowIssue() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
}
}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
index ef9e13f7ccf..51980b2d971 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -18,9 +18,13 @@ export default {
required: true,
type: Object,
},
+ sections: {
+ required: true,
+ type: Object,
+ },
},
maxValue: Object.keys(ACTION_LABELS).length,
- sections: Object.keys(ACTION_SECTIONS),
+ actionSections: Object.keys(ACTION_SECTIONS),
computed: {
progressValue() {
return Object.values(this.actions).filter((a) => a.completed).length;
@@ -38,6 +42,9 @@ export default {
);
return actions;
},
+ svgFor(section) {
+ return this.sections[section].svg;
+ },
},
};
</script>
@@ -59,8 +66,12 @@ export default {
<gl-progress-bar :value="progressValue" :max="$options.maxValue" />
</div>
<div class="row row-cols-1 row-cols-md-3 gl-mt-5">
- <div v-for="section in $options.sections" :key="section" class="col gl-mb-6">
- <learn-gitlab-section-card :section="section" :actions="actionsFor(section)" />
+ <div v-for="section in $options.actionSections" :key="section" class="col gl-mb-6">
+ <learn-gitlab-section-card
+ :section="section"
+ :svg="svgFor(section)"
+ :actions="actionsFor(section)"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
index 6cd3bbc359b..ad6dfbf41ca 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
@@ -64,7 +64,15 @@ export default {
<img :src="svg" :alt="actionLabel" />
<h6>{{ title }}</h6>
<p class="gl-font-sm gl-text-gray-700">{{ description }}</p>
- <gl-link :href="url" target="_blank">{{ actionLabel }}</gl-link>
+ <gl-link
+ :href="url"
+ target="_blank"
+ rel="noopener noreferrer"
+ data-track-action="click_link"
+ :data-track-label="actionLabel"
+ data-track-property="Growth::Activation::Experiment::LearnGitLabB"
+ >{{ actionLabel }}</gl-link
+ >
</div>
</gl-card>
</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
index db694a66afd..6a196687a76 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
@@ -1,6 +1,5 @@
<script>
import { GlCard } from '@gitlab/ui';
-import { imagePath } from '~/lib/utils/common_utils';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
@@ -16,6 +15,10 @@ export default {
required: true,
type: String,
},
+ svg: {
+ required: true,
+ type: String,
+ },
actions: {
required: true,
type: Object,
@@ -28,17 +31,12 @@ export default {
);
},
},
- methods: {
- svg(section) {
- return imagePath(`learn_gitlab/section_${section}.svg`);
- },
- },
};
</script>
<template>
<gl-card class="gl-pt-0 learn-gitlab-section-card">
<div class="learn-gitlab-section-card-header">
- <img :src="svg(section)" />
+ <img :src="svg" />
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
</div>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 6f51c7372fd..3d31ac6c267 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -34,7 +34,15 @@ export default {
{{ $options.i18n.ACTION_LABELS[action].title }}
</span>
<span v-else>
- <gl-link :href="value.url">{{ $options.i18n.ACTION_LABELS[action].title }}</gl-link>
+ <gl-link
+ target="_blank"
+ :href="value.url"
+ data-track-action="click_link"
+ :data-track-label="$options.i18n.ACTION_LABELS[action].title"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ >
+ {{ $options.i18n.ACTION_LABELS[action].title }}
+ </gl-link>
</span>
<span v-if="trialOnly" class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
- {{ $options.i18n.trialOnly }}
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index c4dec89b984..ac7c94bdd9e 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import trackLearnGitlab from '~/learn_gitlab/track_learn_gitlab';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlabA from '../components/learn_gitlab_a.vue';
import LearnGitlabB from '../components/learn_gitlab_b.vue';
@@ -11,13 +12,18 @@ function initLearnGitlab() {
}
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
+ const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const { learnGitlabA } = gon.experiments;
+ trackLearnGitlab(learnGitlabA);
+
return new Vue({
el,
render(createElement) {
- return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
+ return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, {
+ props: { actions, sections },
+ });
},
});
}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index 1a0fa6e544e..8d152ec4ba6 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
-import initCompareAutocomplete from '~/compare_autocomplete';
import axios from '~/lib/utils/axios_utils';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import initCompareAutocomplete from './compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
index 314e4e911ee..68ab7021cf3 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -2,11 +2,11 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
import { fixTitle } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
-import axios from './lib/utils/axios_utils';
-import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-import { __ } from './locale';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function () {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index a5118e3529a..6cd3202815b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,16 +1,17 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
-import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
-import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
+import StatusBox from '~/issuable/components/status_box.vue';
+import createDefaultClient from '~/lib/graphql';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
+import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
@@ -28,15 +29,20 @@ export default function initMergeRequestShow() {
} else {
loadAwardsHandler();
}
- initInviteMemberModal();
- initInviteMemberTrigger();
initInviteMembersModal();
initInviteMembersTrigger();
const el = document.querySelector('.js-mr-status-box');
+ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
+ provide: {
+ query: getStateQuery,
+ projectPath: el.dataset.projectPath,
+ iid: el.dataset.iid,
+ },
render(h) {
return h(StatusBox, {
props: {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
new file mode 100644
index 00000000000..b5a82b9428e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/queries/get_state.query.graphql
@@ -0,0 +1,7 @@
+query getMergeRequestState($projectPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $projectPath) {
+ issuable: mergeRequest(iid: $iid) {
+ state
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js
index 364b0d95d9c..4f8514a9a1d 100644
--- a/app/assets/javascripts/pages/projects/milestones/new/index.js
+++ b/app/assets/javascripts/pages/projects/milestones/new/index.js
@@ -1,3 +1,3 @@
-import initForm from '../../../../shared/milestones/form';
+import initForm from '~/shared/milestones/form';
initForm();
diff --git a/app/assets/javascripts/pages/projects/new/components/app.vue b/app/assets/javascripts/pages/projects/new/components/app.vue
new file mode 100644
index 00000000000..60a4fbc3e6b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/components/app.vue
@@ -0,0 +1,148 @@
+<script>
+import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg';
+import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
+import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
+import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { experiment } from '~/experimentation/utils';
+import { s__ } from '~/locale';
+import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
+import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
+
+const NEW_REPO_EXPERIMENT = 'new_repo';
+const CI_CD_PANEL = 'cicd_for_external_repo';
+const PANELS = [
+ {
+ key: 'blank',
+ name: 'blank_project',
+ selector: '#blank-project-pane',
+ title: s__('ProjectsNew|Create blank project'),
+ description: s__(
+ 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.',
+ ),
+ illustration: blankProjectIllustration,
+ },
+ {
+ key: 'template',
+ name: 'create_from_template',
+ selector: '#create-from-template-pane',
+ title: s__('ProjectsNew|Create from template'),
+ description: s__(
+ 'ProjectsNew|Create a project pre-populated with the necessary files to get you started quickly.',
+ ),
+ illustration: createFromTemplateIllustration,
+ },
+ {
+ key: 'import',
+ name: 'import_project',
+ selector: '#import-project-pane',
+ title: s__('ProjectsNew|Import project'),
+ description: s__(
+ 'ProjectsNew|Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
+ ),
+ illustration: importProjectIllustration,
+ },
+ {
+ key: 'ci',
+ name: CI_CD_PANEL,
+ selector: '#ci-cd-project-pane',
+ title: s__('ProjectsNew|Run CI/CD for external repository'),
+ description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'),
+ illustration: ciCdProjectIllustration,
+ },
+];
+
+export default {
+ components: {
+ NewNamespacePage,
+ NewProjectPushTipPopover,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ hasErrors: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isCiCdAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ newProjectGuidelines: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ computed: {
+ decoratedPanels() {
+ const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
+ use: () => ({
+ blank: s__('ProjectsNew|Create blank project'),
+ import: s__('ProjectsNew|Import project'),
+ }),
+ try: () => ({
+ blank: s__('ProjectsNew|Create blank project/repository'),
+ import: s__('ProjectsNew|Import project/repository'),
+ }),
+ });
+
+ return PANELS.map(({ key, title, ...el }) => ({
+ ...el,
+ title: PANEL_TITLES[key] ?? title,
+ }));
+ },
+
+ availablePanels() {
+ return this.isCiCdAvailable
+ ? this.decoratedPanels
+ : this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
+ },
+ },
+
+ methods: {
+ resetProjectErrors() {
+ const errorsContainer = document.querySelector('.project-edit-errors');
+ if (errorsContainer) {
+ errorsContainer.innerHTML = '';
+ }
+ },
+ },
+ EXPERIMENT: NEW_REPO_EXPERIMENT,
+};
+</script>
+
+<template>
+ <new-namespace-page
+ :initial-breadcrumb="s__('New project')"
+ :panels="availablePanels"
+ :jump-to-last-persisted-panel="hasErrors"
+ :title="s__('ProjectsNew|Create new project')"
+ :experiment="$options.EXPERIMENT"
+ persistence-key="new_project_last_active_tab"
+ @panel-change="resetProjectErrors"
+ >
+ <template #extra-description>
+ <div
+ v-if="newProjectGuidelines"
+ id="new-project-guideline"
+ v-safe-html="newProjectGuidelines"
+ ></div>
+ </template>
+ <template #welcome-footer>
+ <div class="gl-pt-5 gl-text-center">
+ <p>
+ {{ __('You can also create a project from the command line.') }}
+ <a ref="clipTip" href="#" @click.prevent>
+ {{ __('Show command') }}
+ </a>
+ <new-project-push-tip-popover :target="() => $refs.clipTip" />
+ </p>
+ </div>
+ </template>
+ </new-namespace-page>
+</template>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue
index e42d9154866..e42d9154866 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue
+++ b/app/assets/javascripts/pages/projects/new/components/new_project_push_tip_popover.vue
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index e10e2872dce..f469c56e808 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,28 +1,44 @@
-import { deprecatedCreateFlash as createFlash } from '~/flash';
-import { __ } from '~/locale';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
+import NewProjectCreationApp from './components/app.vue';
initProjectVisibilitySelector();
initProjectNew.bindEvents();
-import(
- /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
-)
- .then((m) => {
- const el = document.querySelector('.js-experiment-new-project-creation');
+function initNewProjectCreation(el) {
+ const {
+ pushToCreateProjectCommand,
+ workingWithProjectsHelpPath,
+ newProjectGuidelines,
+ hasErrors,
+ isCiCdAvailable,
+ } = el.dataset;
- if (!el) {
- return;
- }
+ const props = {
+ hasErrors: parseBoolean(hasErrors),
+ isCiCdAvailable: parseBoolean(isCiCdAvailable),
+ newProjectGuidelines,
+ };
- const config = {
- hasErrors: 'hasErrors' in el.dataset,
- isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
- newProjectGuidelines: el.dataset.newProjectGuidelines,
- };
- m.default(el, config);
- })
- .catch(() => {
- createFlash(__('An error occurred while loading project creation UI'));
+ const provide = {
+ workingWithProjectsHelpPath,
+ pushToCreateProjectCommand,
+ };
+
+ return new Vue({
+ el,
+ components: {
+ NewProjectCreationApp,
+ },
+ provide,
+ render(h) {
+ return h(NewProjectCreationApp, { props });
+ },
});
+}
+
+const el = document.querySelector('.js-new-project-creation');
+
+initNewProjectCreation(el);
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 32299287a9c..e1f71965853 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,17 +1,3 @@
-import $ from 'jquery';
-import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
-import NewBranchForm from '~/new_branch_form';
-import initNewPipeline from '~/pipeline_new/index';
+import initNewPipelineForm from '~/pipeline_new/index';
-const el = document.getElementById('js-new-pipeline');
-
-if (el) {
- initNewPipeline();
-} else {
- new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
-
- setupNativeFormVariableList({
- container: $('.js-ci-variable-list-section'),
- formField: 'variables_attributes',
- });
-}
+initNewPipelineForm();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index be9259ec3ca..10105af3561 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -3,9 +3,9 @@ import SecretValues from '~/behaviors/secret_values';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
+import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
-import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
@@ -36,10 +36,6 @@ document.addEventListener('DOMContentLoaded', () => {
initSettingsPipelinesTriggers();
initArtifactsSettings();
-
- if (gon?.features?.vueifySharedRunnersToggle) {
- initSharedRunnersToggle();
- }
-
+ initSharedRunnersToggle();
initInstallRunner();
});
diff --git a/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js b/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
new file mode 100644
index 00000000000..93c6a2c63a3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/packages_and_registries/show/index.js
@@ -0,0 +1,5 @@
+import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
+import initSettingsPanels from '~/settings_panels';
+
+registrySettingsApp();
+initSettingsPanels();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index 8d390c8586b..380091a3501 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
+import initDatePicker from '~/behaviors/date_picker';
import initDeployKeys from '~/deploy_keys';
-import DueDateSelectors from '~/due_date_select';
import fileUpload from '~/lib/utils/file_upload';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
@@ -16,6 +16,6 @@ export default () => {
initSettingsPanels();
new ProtectedBranchCreate({ hasLicense: false });
new ProtectedBranchEditList();
- new DueDateSelectors();
+ initDatePicker(); // Used for deploy token "expires at" field
fileUpload('.js-choose-file', '.js-object-map-input');
};
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index f955a41e18a..c719601ee0b 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -1 +1,9 @@
import '~/snippet/snippet_show';
+
+const awardEmojiEl = document.getElementById('js-vue-awards-block');
+
+if (awardEmojiEl) {
+ import('~/emoji/awards_app')
+ .then((m) => m.default(awardEmojiEl))
+ .catch(() => {});
+}
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 6afc33ec8a5..43753926039 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -1,9 +1,21 @@
<script>
-import { GlForm, GlIcon, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
+import {
+ GlForm,
+ GlIcon,
+ GlLink,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ GlLoadingIcon,
+ GlModal,
+ GlModalDirective,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
-import { __, s__, sprintf } from '~/locale';
+import { s__, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const MARKDOWN_LINK_TEXT = {
markdown: '[Link Title](page-slug)',
@@ -13,21 +25,98 @@ const MARKDOWN_LINK_TEXT = {
};
export default {
+ i18n: {
+ title: {
+ label: s__('WikiPage|Title'),
+ placeholder: s__('WikiPage|Page title'),
+ helpText: {
+ existingPage: s__(
+ 'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
+ ),
+ newPage: s__(
+ 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
+ ),
+ moreInformation: s__('WikiPage|More Information.'),
+ },
+ },
+ format: {
+ label: s__('WikiPage|Format'),
+ },
+ content: {
+ label: s__('WikiPage|Content'),
+ placeholder: s__('WikiPage|Write your content or drag files here…'),
+ },
+ contentEditor: {
+ renderFailed: {
+ message: s__(
+ 'WikiPage|An error occured while trying to render the content editor. Please try again later.',
+ ),
+ primaryAction: s__('WikiPage|Retry'),
+ },
+ useNewEditor: s__('WikiPage|Use new editor'),
+ switchToOldEditor: {
+ label: s__('WikiPage|Switch to old editor'),
+ helpText: s__("WikiPage|Switching will discard any changes you've made in the new editor."),
+ modal: {
+ title: s__('WikiPage|Are you sure you want to switch to the old editor?'),
+ primary: s__('WikiPage|Switch to old editor'),
+ cancel: s__('WikiPage|Keep editing'),
+ text: s__(
+ "WikiPage|Switching to the old editor will discard any changes you've made in the new editor.",
+ ),
+ },
+ },
+ helpText: s__(
+ "WikiPage|This editor is in beta and may not display the page's contents properly.",
+ ),
+ },
+ linksHelpText: s__(
+ 'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
+ ),
+ commitMessage: {
+ label: s__('WikiPage|Commit message'),
+ value: {
+ existingPage: s__('WikiPage|Update %{pageTitle}'),
+ newPage: s__('WikiPage|Create %{pageTitle}'),
+ },
+ },
+ submitButton: {
+ existingPage: s__('WikiPage|Save changes'),
+ newPage: s__('WikiPage|Create page'),
+ },
+ cancel: s__('WikiPage|Cancel'),
+ },
components: {
+ GlAlert,
GlForm,
GlSprintf,
GlIcon,
GlLink,
GlButton,
+ GlModal,
MarkdownField,
+ GlLoadingIcon,
+ ContentEditor: () =>
+ import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
+ ),
+ },
+ directives: {
+ GlModalDirective,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
title: this.pageInfo.title?.trim() || '',
format: this.pageInfo.format || 'markdown',
content: this.pageInfo.content?.trim() || '',
+ isContentEditorLoading: true,
+ useContentEditor: false,
commitMessage: '',
+ contentEditor: null,
+ isDirty: false,
+ contentEditorRenderFailed: false,
};
},
computed: {
@@ -45,15 +134,21 @@ export default {
},
commitMessageI18n() {
return this.pageInfo.persisted
- ? s__('WikiPage|Update %{pageTitle}')
- : s__('WikiPage|Create %{pageTitle}');
+ ? this.$options.i18n.commitMessage.value.existingPage
+ : this.$options.i18n.commitMessage.value.newPage;
},
linkExample() {
return MARKDOWN_LINK_TEXT[this.format];
},
submitButtonText() {
- if (this.pageInfo.persisted) return __('Save changes');
- return s__('WikiPage|Create page');
+ return this.pageInfo.persisted
+ ? this.$options.i18n.submitButton.existingPage
+ : this.$options.i18n.submitButton.newPage;
+ },
+ titleHelpText() {
+ return this.pageInfo.persisted
+ ? this.$options.i18n.title.helpText.existingPage
+ : this.$options.i18n.title.helpText.newPage;
},
cancelFormPath() {
if (this.pageInfo.persisted) return this.pageInfo.path;
@@ -62,20 +157,53 @@ export default {
wikiSpecificMarkdownHelpPath() {
return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown');
},
+ isMarkdownFormat() {
+ return this.format === 'markdown';
+ },
+ showContentEditorButton() {
+ return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor;
+ },
+ disableSubmitButton() {
+ return !this.content || !this.title || this.contentEditorRenderFailed;
+ },
+ isContentEditorActive() {
+ return this.isMarkdownFormat && this.useContentEditor;
+ },
},
mounted() {
this.updateCommitMessage();
+
+ window.addEventListener('beforeunload', this.onPageUnload);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
+ getContentHTML(content) {
+ return axios
+ .post(this.pageInfo.markdownPreviewPath, { text: content })
+ .then(({ data }) => data.body);
+ },
+
handleFormSubmit() {
- window.removeEventListener('beforeunload', this.onBeforeUnload);
+ if (this.useContentEditor) {
+ this.content = this.contentEditor.getSerializedContent();
+ }
+
+ this.isDirty = false;
},
handleContentChange() {
- window.addEventListener('beforeunload', this.onBeforeUnload);
+ this.isDirty = true;
},
- onBeforeUnload() {
+ onPageUnload(event) {
+ if (!this.isDirty) return undefined;
+
+ event.preventDefault();
+
+ // eslint-disable-next-line no-param-reassign
+ event.returnValue = '';
return '';
},
@@ -88,6 +216,48 @@ export default {
const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: newTitle }, false);
this.commitMessage = newCommitMessage;
},
+
+ async initContentEditor() {
+ this.isContentEditorLoading = true;
+ this.useContentEditor = true;
+
+ const { createContentEditor } = await import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_content_editor'
+ );
+ this.contentEditor =
+ this.contentEditor ||
+ createContentEditor({
+ renderMarkdown: (markdown) => this.getContentHTML(markdown),
+ tiptapOptions: {
+ onUpdate: () => this.handleContentChange(),
+ },
+ });
+
+ try {
+ await this.contentEditor.setSerializedContent(this.content);
+ this.isContentEditorLoading = false;
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ retryInitContentEditor() {
+ this.contentEditorRenderFailed = false;
+ this.initContentEditor();
+ },
+
+ switchToOldEditor() {
+ this.useContentEditor = false;
+ },
+
+ confirmSwitchToOldEditor() {
+ if (this.contentEditorRenderFailed) {
+ this.contentEditorRenderFailed = false;
+ this.switchToOldEditor();
+ } else {
+ this.$refs.confirmSwitchToOldEditorModal.show();
+ }
+ },
},
};
</script>
@@ -99,6 +269,19 @@ export default {
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
>
+ <gl-alert
+ v-if="isContentEditorActive && contentEditorRenderFailed"
+ class="gl-mb-6"
+ :dismissible="false"
+ variant="danger"
+ :primary-button-text="$options.i18n.contentEditor.renderFailed.primaryAction"
+ @primaryAction="retryInitContentEditor()"
+ >
+ <p>
+ {{ $options.i18n.contentEditor.renderFailed.message }}
+ </p>
+ </gl-alert>
+
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
<input
@@ -109,7 +292,9 @@ export default {
/>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
- <label class="control-label-full-width" for="wiki_title">{{ s__('WikiPage|Title') }}</label>
+ <label class="control-label-full-width" for="wiki_title">{{
+ $options.i18n.title.label
+ }}</label>
</div>
<div class="col-sm-10">
<input
@@ -121,22 +306,15 @@ export default {
data-qa-selector="wiki_title_textbox"
:required="true"
:autofocus="!pageInfo.persisted"
- :placeholder="s__('WikiPage|Page title')"
+ :placeholder="$options.i18n.title.placeholder"
@input="updateCommitMessage"
/>
<span class="gl-display-inline-block gl-max-w-full gl-mt-2 gl-text-gray-600">
<gl-icon class="gl-mr-n1" name="bulb" />
- {{
- pageInfo.persisted
- ? s__(
- 'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
- )
- : s__(
- 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
- )
- }}
- <gl-link :href="helpPath" target="_blank" data-testid="wiki-title-help-link"
- ><gl-icon name="question-o" /> {{ __('More Information.') }}</gl-link
+ {{ titleHelpText }}
+ <gl-link :href="helpPath" target="_blank"
+ ><gl-icon name="question-o" />
+ {{ $options.i18n.title.helpText.moreInformation }}</gl-link
>
</span>
</div>
@@ -144,25 +322,63 @@ export default {
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_format">{{
- s__('WikiPage|Format')
+ $options.i18n.format.label
}}</label>
</div>
<div class="col-sm-10">
- <select id="wiki_format" v-model="format" class="form-control" name="wiki[format]">
+ <select
+ id="wiki_format"
+ v-model="format"
+ class="form-control"
+ name="wiki[format]"
+ :disabled="isContentEditorActive"
+ >
<option v-for="(key, label) of formatOptions" :key="key" :value="key">
{{ label }}
</option>
</select>
+ <div>
+ <gl-button
+ v-if="showContentEditorButton"
+ category="secondary"
+ variant="confirm"
+ class="gl-mt-4"
+ @click="initContentEditor"
+ >{{ $options.i18n.contentEditor.useNewEditor }}</gl-button
+ >
+ <div v-if="isContentEditorActive" class="gl-mt-4 gl-display-flex">
+ <div class="gl-mr-4">
+ <gl-button category="secondary" variant="confirm" @click="confirmSwitchToOldEditor">{{
+ $options.i18n.contentEditor.switchToOldEditor.label
+ }}</gl-button>
+ </div>
+ <div class="gl-mt-2">
+ <gl-icon name="warning" />
+ {{ $options.i18n.contentEditor.switchToOldEditor.helpText }}
+ </div>
+ </div>
+ <gl-modal
+ ref="confirmSwitchToOldEditorModal"
+ modal-id="confirm-switch-to-old-editor"
+ :title="$options.i18n.contentEditor.switchToOldEditor.modal.title"
+ :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }"
+ :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }"
+ @primary="switchToOldEditor"
+ >
+ {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }}
+ </gl-modal>
+ </div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_content">{{
- s__('WikiPage|Content')
+ $options.i18n.content.label
}}</label>
</div>
<div class="col-sm-10">
<markdown-field
+ v-if="!isContentEditorActive"
:markdown-preview-path="pageInfo.markdownPreviewPath"
:can-attach-file="true"
:enable-autocomplete="true"
@@ -182,24 +398,25 @@ export default {
data-supports-quick-actions="false"
data-qa-selector="wiki_content_textarea"
:autofocus="pageInfo.persisted"
- :aria-label="s__('WikiPage|Content')"
- :placeholder="s__('WikiPage|Write your content or drag files here…')"
+ :aria-label="$options.i18n.content.label"
+ :placeholder="$options.i18n.content.placeholder"
@input="handleContentChange"
>
</textarea>
</template>
</markdown-field>
+
+ <div v-if="isContentEditorActive">
+ <gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" />
+ <content-editor v-else :content-editor="contentEditor" />
+ <input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
+ </div>
+
<div class="clearfix"></div>
<div class="error-alert"></div>
<div class="form-text gl-text-gray-600">
- <gl-sprintf
- :message="
- s__(
- 'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
- )
- "
- >
+ <gl-sprintf v-if="!isContentEditorActive" :message="$options.i18n.linksHelpText">
<template #linkExample
><code>{{ linkExample }}</code></template
>
@@ -214,13 +431,16 @@ export default {
></template
>
</gl-sprintf>
+ <span v-else>
+ {{ $options.i18n.contentEditor.helpText }}
+ </span>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2 col-form-label">
<label class="control-label-full-width" for="wiki_message">{{
- s__('WikiPage|Commit message')
+ $options.i18n.commitMessage.label
}}</label>
</div>
<div class="col-sm-10">
@@ -231,7 +451,7 @@ export default {
type="text"
class="form-control"
data-qa-selector="wiki_message_textbox"
- :placeholder="s__('WikiPage|Commit message')"
+ :placeholder="$options.i18n.commitMessage.label"
/>
</div>
</div>
@@ -242,12 +462,10 @@ export default {
type="submit"
data-qa-selector="wiki_submit_button"
data-testid="wiki-submit-button"
- :disabled="!content || !title"
+ :disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
- <gl-button :href="cancelFormPath" class="float-right" data-testid="wiki-cancel-button">{{
- __('Cancel')
- }}</gl-button>
+ <gl-button :href="cancelFormPath" class="float-right">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index d236dc4610a..c416106fdd8 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -247,7 +247,7 @@ export default class ActivityCalendar {
renderKey() {
const keyValues = [
- __('no contributions'),
+ __('No contributions'),
__('1-9 contributions'),
__('10-19 contributions'),
__('20-29 contributions'),
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 80e14842f51..f9d70845560 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -223,14 +223,14 @@ export default class UserTabs {
.then((data) => UserTabs.renderActivityCalendar(data, $calendarWrap))
.catch(() => {
const cWrap = $calendarWrap[0];
- cWrap.querySelector('.spinner').classList.add('invisible');
+ cWrap.querySelector('.gl-spinner').classList.add('invisible');
cWrap.querySelector('.user-calendar-error').classList.remove('invisible');
cWrap
.querySelector('.user-calendar-error .js-retry-load')
.addEventListener('click', (e) => {
e.preventDefault();
cWrap.querySelector('.user-calendar-error').classList.add('invisible');
- cWrap.querySelector('.spinner').classList.remove('invisible');
+ cWrap.querySelector('.gl-spinner').classList.remove('invisible');
this.loadActivityCalendar();
});
});
diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js
index 98b2e4238c1..1db80057d0c 100644
--- a/app/assets/javascripts/performance/constants.js
+++ b/app/assets/javascripts/performance/constants.js
@@ -43,6 +43,7 @@ export const WEBIDE_MEASURE_FETCH_FILES = 'WebIDE: Fetch Files';
//
// MR Diffs namespace
+//
// Marks
export const MR_DIFFS_MARK_FILE_TREE_START = 'mr-diffs-mark-file-tree-start';
@@ -75,3 +76,14 @@ export const PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION =
export const PIPELINES_DETAIL_LINK_DURATION = 'pipeline_graph_link_calculation_duration_seconds';
export const PIPELINES_DETAIL_LINKS_TOTAL = 'pipeline_graph_links_total';
export const PIPELINES_DETAIL_LINKS_JOB_RATIO = 'pipeline_graph_links_per_job_ratio';
+
+//
+// REPO BROWSER NAMESPACE
+//
+
+// Marks
+export const REPO_BLOB_LOAD_VIEWER_START = 'blobviewer-load-viewer-start';
+export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish';
+
+// Measures
+export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the content';
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 14a4a9d5710..567164cb0ee 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -11,6 +11,7 @@ import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphq
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
+import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
import CommitForm from './commit_form.vue';
@@ -94,10 +95,15 @@ export default {
},
update(store, { data }) {
const commitSha = data?.commitCreate?.commit?.sha;
+ const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (commitSha) {
store.writeQuery({ query: getCommitSha, data: { commitSha } });
}
+
+ if (pipelineEtag) {
+ store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
+ }
},
});
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
new file mode 100644
index 00000000000..22c1563350d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineVisualReference from '../ui/pipeline_visual_reference.vue';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|🚀 Run your first pipeline'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|A typical GitLab pipeline consists of three stages: build, test and deploy. Each stage can have one or more jobs.',
+ ),
+ secondParagraph: s__(
+ 'PipelineEditorTutorial|In the example below, %{codeStart}build%{codeEnd} and %{codeStart}deploy%{codeEnd} each contain one job, and %{codeStart}test%{codeEnd} contains two jobs. Your scripts run in jobs like these.',
+ ),
+ thirdParagraph: s__(
+ 'PipelineEditorTutorial|You can use %{linkStart}CI/CD examples and templates%{linkEnd} to get your first %{codeStart}.gitlab-ci.yml%{codeEnd} configuration file started. Your first pipeline runs when you commit the changes.',
+ ),
+ note: s__(
+ 'PipelineEditorTutorial|If you’re using a self-managed GitLab instance, %{linkStart}make sure your instance has runners available.%{linkEnd}',
+ ),
+ },
+ components: {
+ GlCard,
+ GlLink,
+ GlSprintf,
+ PipelineVisualReference,
+ },
+ inject: ['ciExamplesHelpPagePath', 'runnerHelpPagePath'],
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <p class="gl-mb-3">
+ <gl-sprintf :message="$options.i18n.secondParagraph">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <pipeline-visual-reference />
+ <p class="gl-my-3">
+ <gl-sprintf :message="$options.i18n.thirdParagraph">
+ <template #link="{ content }">
+ <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.note">
+ <template #link="{ content }">
+ <gl-link :href="runnerHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
new file mode 100644
index 00000000000..3da535f5f94
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlCard, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|Get started with GitLab CI/CD'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|GitLab CI/CD can automatically build, test, and deploy your application.',
+ ),
+ secondParagraph: s__(
+ 'PipelineEditorTutorial|The pipeline stages and jobs are defined in a %{codeStart}.gitlab-ci.yml%{codeEnd} file. You can edit, visualize and validate the syntax in this file by using the Pipeline Editor.',
+ ),
+ },
+ components: {
+ GlCard,
+ GlSprintf,
+ },
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.secondParagraph">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
new file mode 100644
index 00000000000..f714f6411f1
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|⚙️ Pipeline configuration reference'),
+ firstParagraph: s__('PipelineEditorTutorial|Resources to help with your CI/CD configuration:'),
+ browseExamples: s__(
+ 'PipelineEditorTutorial|Browse %{linkStart}CI/CD examples and templates%{linkEnd}',
+ ),
+ viewSyntaxRef: s__(
+ 'PipelineEditorTutorial|View %{linkStart}.gitlab-ci.yml syntax reference%{linkEnd}',
+ ),
+ learnMore: s__(
+ 'PipelineEditorTutorial|Learn more about %{linkStart}GitLab CI/CD concepts%{linkEnd}',
+ ),
+ needs: s__(
+ 'PipelineEditorTutorial|Make your pipeline more efficient with the %{linkStart}Needs keyword%{linkEnd}',
+ ),
+ },
+ components: {
+ GlCard,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['ciExamplesHelpPagePath', 'ciHelpPagePath', 'needsHelpPagePath', 'ymlHelpPagePath'],
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <ul>
+ <li>
+ <gl-sprintf :message="$options.i18n.browseExamples">
+ <template #link="{ content }">
+ <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.viewSyntaxRef">
+ <template #link="{ content }">
+ <gl-link :href="ymlHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.learnMore">
+ <template #link="{ content }">
+ <gl-link :href="ciHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.needs">
+ <template #link="{ content }">
+ <gl-link :href="needsHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
new file mode 100644
index 00000000000..512414f0246
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlCard } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|💡 Tip: Visualize and validate your pipeline'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|Use the Visualize and Lint tabs in the Pipeline Editor to visualize your pipeline and check for any errors or warnings before committing your changes.',
+ ),
+ },
+ components: {
+ GlCard,
+ },
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-0">{{ $options.i18n.firstParagraph }}</p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
new file mode 100644
index 00000000000..ff1e0b6388f
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -0,0 +1,105 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { DRAWER_EXPANDED_KEY } from '../../constants';
+import FirstPipelineCard from './cards/first_pipeline_card.vue';
+import GettingStartedCard from './cards/getting_started_card.vue';
+import PipelineConfigReferenceCard from './cards/pipeline_config_reference_card.vue';
+import VisualizeAndLintCard from './cards/visualize_and_lint_card.vue';
+
+export default {
+ width: {
+ expanded: '482px',
+ collapsed: '58px',
+ },
+ i18n: {
+ toggleTxt: __('Collapse'),
+ },
+ localDrawerKey: DRAWER_EXPANDED_KEY,
+ components: {
+ FirstPipelineCard,
+ GettingStartedCard,
+ GlButton,
+ GlIcon,
+ LocalStorageSync,
+ PipelineConfigReferenceCard,
+ VisualizeAndLintCard,
+ },
+ data() {
+ return {
+ isExpanded: false,
+ topPosition: 0,
+ };
+ },
+ computed: {
+ buttonIconName() {
+ return this.isExpanded ? 'chevron-double-lg-right' : 'chevron-double-lg-left';
+ },
+ buttonClass() {
+ return this.isExpanded ? 'gl-justify-content-end!' : '';
+ },
+ rootStyle() {
+ const { expanded, collapsed } = this.$options.width;
+ const top = this.topPosition;
+ const style = { top: `${top}px` };
+
+ return this.isExpanded ? { ...style, width: expanded } : { ...style, width: collapsed };
+ },
+ },
+ mounted() {
+ this.setTopPosition();
+ this.setInitialExpandState();
+ },
+ methods: {
+ setInitialExpandState() {
+ // We check in the local storage and if no value is defined, we want the default
+ // to be true. We want to explicitly set it to true here so that the drawer
+ // animates to open on load.
+ const localValue = localStorage.getItem(this.$options.localDrawerKey);
+ if (localValue === null) {
+ this.isExpanded = true;
+ }
+ },
+ setTopPosition() {
+ const navbarEl = document.querySelector('.js-navbar');
+
+ if (navbarEl) {
+ this.topPosition = navbarEl.getBoundingClientRect().bottom;
+ }
+ },
+ toggleDrawer() {
+ this.isExpanded = !this.isExpanded;
+ },
+ },
+};
+</script>
+<template>
+ <local-storage-sync v-model="isExpanded" :storage-key="$options.localDrawerKey" as-json>
+ <aside
+ aria-live="polite"
+ class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-property-width gl-transition-duration-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-3 gl-overflow-y-auto"
+ :style="rootStyle"
+ >
+ <gl-button
+ category="tertiary"
+ class="gl-w-full gl-h-9 gl-rounded-0! gl-border-none! gl-border-b-solid! gl-border-1! gl-border-gray-100 gl-text-decoration-none! gl-outline-0! gl-display-flex"
+ :class="buttonClass"
+ :title="__('Toggle sidebar')"
+ @click="toggleDrawer"
+ >
+ <span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">
+ {{ __('Collapse') }}
+ </span>
+ <gl-icon data-testid="toggle-icon" :name="buttonIconName" />
+ </gl-button>
+ <div v-if="isExpanded" class="gl-h-full gl-p-5" data-testid="drawer-content">
+ <getting-started-card class="gl-mb-4" />
+ <first-pipeline-card class="gl-mb-4" />
+ <visualize-and-lint-card class="gl-mb-4" />
+ <pipeline-config-reference-card />
+ <div class="gl-h-13"></div>
+ </div>
+ </aside>
+ </local-storage-sync>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue b/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
new file mode 100644
index 00000000000..049504181c4
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ props: {
+ jobName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-w-13 gl-h-6 gl-font-sm gl-bg-white gl-inset-border-1-blue-500 gl-text-center gl-text-truncate gl-rounded-pill gl-px-4 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
+ >
+ {{ jobName }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue b/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue
new file mode 100644
index 00000000000..1017237365b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue
@@ -0,0 +1,43 @@
+<script>
+import { s__ } from '~/locale';
+import DemoJobPill from './demo_job_pill.vue';
+
+export default {
+ i18n: {
+ stageNames: {
+ build: s__('StageName|Build'),
+ test: s__('StageName|Test'),
+ deploy: s__('StageName|Deploy'),
+ },
+ jobNames: {
+ build: s__('JobName|build-job'),
+ test_1: s__('JobName|unit-test'),
+ test_2: s__('JobName|lint-test'),
+ deploy: s__('JobName|deploy-app'),
+ },
+ },
+ stageClasses:
+ 'gl-bg-blue-50 gl-display-flex gl-flex-direction-column gl-align-items-center gl-p-4 gl-rounded-base',
+ titleClasses: 'gl-text-blue-600 gl-mb-4',
+ components: {
+ DemoJobPill,
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-justify-content-center">
+ <div :class="$options.stageClasses" class="gl-mr-5">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.build }}</div>
+ <demo-job-pill :job-name="$options.i18n.jobNames.build" />
+ </div>
+ <div :class="$options.stageClasses" class="gl-mr-5">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.test }}</div>
+ <demo-job-pill class="gl-mb-3" :job-name="$options.i18n.jobNames.test_1" />
+ <demo-job-pill :job-name="$options.i18n.jobNames.test_2" />
+ </div>
+ <div :class="$options.stageClasses">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.deploy }}</div>
+ <demo-job-pill :job-name="$options.i18n.jobNames.deploy" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index b3eba0fcc19..1acf3a03e73 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -1,32 +1,77 @@
<script>
-import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlIcon } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlInfiniteScroll,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { historyPushState } from '~/lib/utils/common_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
+import {
+ BRANCH_PAGINATION_LIMIT,
+ BRANCH_SEARCH_DEBOUNCE,
+ DEFAULT_FAILURE,
+} from '~/pipeline_editor/constants';
import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.graphql';
export default {
i18n: {
+ dropdownHeader: s__('Switch Branch'),
title: s__('Branches'),
fetchError: s__('Unable to fetch branch list for this project.'),
},
+ inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlIcon,
+ GlInfiniteScroll,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ },
+ inject: ['projectFullPath', 'totalBranches'],
+ props: {
+ paginationLimit: {
+ type: Number,
+ required: false,
+ default: BRANCH_PAGINATION_LIMIT,
+ },
+ },
+ data() {
+ return {
+ branches: [],
+ page: {
+ limit: this.paginationLimit,
+ offset: 0,
+ searchTerm: '',
+ },
+ };
},
- inject: ['projectFullPath'],
apollo: {
- branches: {
+ availableBranches: {
query: getAvailableBranches,
variables() {
return {
+ limit: this.page.limit,
+ offset: this.page.offset,
projectFullPath: this.projectFullPath,
+ searchPattern: this.searchPattern,
};
},
update(data) {
- return data.project?.repository?.branches || [];
+ return data.project?.repository?.branchNames || [];
+ },
+ result({ data }) {
+ const newBranches = data.project?.repository?.branchNames || [];
+
+ // check that we're not re-concatenating existing fetch results
+ if (!this.branches.includes(newBranches[0])) {
+ this.branches = this.branches.concat(newBranches);
+ }
},
error() {
this.$emit('showError', {
@@ -40,26 +85,99 @@ export default {
},
},
computed: {
- hasBranchList() {
- return this.branches?.length > 0;
+ isBranchesLoading() {
+ return this.$apollo.queries.availableBranches.loading;
+ },
+ showBranchSwitcher() {
+ return this.branches.length > 0 || this.page.searchTerm.length > 0;
+ },
+ searchPattern() {
+ if (this.page.searchTerm === '') {
+ return '*';
+ }
+
+ return `*${this.page.searchTerm}*`;
+ },
+ },
+ methods: {
+ // if there is no searchPattern, paginate by {paginationLimit} branches
+ fetchNextBranches() {
+ if (
+ this.isBranchesLoading ||
+ this.page.searchTerm.length > 0 ||
+ this.branches.length === this.totalBranches
+ ) {
+ return;
+ }
+
+ this.page = {
+ ...this.page,
+ limit: this.paginationLimit,
+ offset: this.page.offset + this.paginationLimit,
+ };
+ },
+ async selectBranch(newBranch) {
+ if (newBranch === this.currentBranch) {
+ return;
+ }
+
+ await this.$apollo.getClient().writeQuery({
+ query: getCurrentBranch,
+ data: { currentBranch: newBranch },
+ });
+
+ const updatedPath = setUrlParams({ branch_name: newBranch });
+ historyPushState(updatedPath);
+
+ this.$emit('refetchContent');
+ },
+ setSearchTerm(newSearchTerm) {
+ this.branches = [];
+ this.page = {
+ limit: newSearchTerm.trim() === '' ? this.paginationLimit : this.totalBranches,
+ offset: 0,
+ searchTerm: newSearchTerm.trim(),
+ };
},
},
};
</script>
<template>
- <gl-dropdown v-if="hasBranchList" class="gl-ml-2" :text="currentBranch" icon="branch">
+ <gl-dropdown
+ v-if="showBranchSwitcher"
+ class="gl-ml-2"
+ :header-text="$options.i18n.dropdownHeader"
+ :text="currentBranch"
+ icon="branch"
+ >
+ <gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
<gl-dropdown-section-header>
- {{ this.$options.i18n.title }}
+ {{ $options.i18n.title }}
</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="branch in branches"
- :key="branch.name"
- :is-checked="currentBranch === branch.name"
- :is-check-item="true"
+
+ <gl-infinite-scroll
+ :fetched-items="branches.length"
+ :total-items="totalBranches"
+ :max-list-height="250"
+ @bottomReached="fetchNextBranches"
>
- <gl-icon name="check" class="gl-visibility-hidden" />
- {{ branch.name }}
- </gl-dropdown-item>
+ <template #items>
+ <gl-dropdown-item
+ v-for="branch in branches"
+ :key="branch"
+ :is-checked="currentBranch === branch"
+ :is-check-item="true"
+ @click="selectBranch(branch)"
+ >
+ {{ branch }}
+ </gl-dropdown-item>
+ </template>
+ <template #default>
+ <gl-dropdown-item v-if="isBranchesLoading" key="loading">
+ <gl-loading-icon size="md" />
+ </gl-dropdown-item>
+ </template>
+ </gl-infinite-scroll>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
index fefa784f060..24bca04e115 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -1,5 +1,4 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineStatus from './pipeline_status.vue';
import ValidationSegment from './validation_segment.vue';
@@ -29,7 +28,6 @@ export default {
PipelineStatus,
ValidationSegment,
},
- mixins: [glFeatureFlagsMixin()],
props: {
ciConfigData: {
type: Object,
@@ -42,7 +40,7 @@ export default {
},
computed: {
showPipelineStatus() {
- return this.glFeatures.pipelineStatusForPipelineEditor && !this.isNewCiConfigFile;
+ return !this.isNewCiConfigFile;
},
// make sure corners are rounded correctly depending on if
// pipeline status is rendered
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index 4a92e106da1..368a026bdaa 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -5,7 +5,11 @@ import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
-import { toggleQueryPollingByVisibility } from '~/pipelines/components/graph/utils';
+import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
+import {
+ getQueryHeaders,
+ toggleQueryPollingByVisibility,
+} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
const POLL_INTERVAL = 10000;
@@ -31,7 +35,13 @@ export default {
commitSha: {
query: getCommitSha,
},
+ pipelineEtag: {
+ query: getPipelineEtag,
+ },
pipeline: {
+ context() {
+ return getQueryHeaders(this.pipelineEtag);
+ },
query: getPipelineQuery,
variables() {
return {
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 5acb3355b23..4e2f26af51d 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -110,7 +110,6 @@ export default {
<text-editor :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
- v-if="glFeatures.ciConfigVisualizationTab"
class="gl-mb-3"
:empty-message="$options.i18n.empty.visualization"
:is-empty="isEmpty"
@@ -135,7 +134,6 @@ export default {
<ci-lint v-else :is-valid="isValid" :ci-config="ciConfigData" />
</editor-tab>
<editor-tab
- v-if="glFeatures.ciConfigMergedTab"
class="gl-mb-3"
:empty-message="$options.i18n.empty.merge"
:keep-component-mounted="false"
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index d4f04a0d055..0ac4a40ff4a 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -1,12 +1,14 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
+import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlButton,
GlSprintf,
+ PipelineEditorFileNav,
},
i18n: {
title: __('Optimize your workflow with CI/CD Pipelines'),
@@ -22,6 +24,9 @@ export default {
},
},
computed: {
+ showFileNav() {
+ return this.glFeatures.pipelineEditorBranchSwitcher;
+ },
showCTAButton() {
return this.glFeatures.pipelineEditorEmptyStateAction;
},
@@ -34,23 +39,26 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
- <img :src="emptyStateIllustrationPath" />
- <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
- <p class="gl-mt-3">
- <gl-sprintf :message="$options.i18n.body">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- <gl-button
- v-if="showCTAButton"
- variant="confirm"
- class="gl-mt-3"
- @click="createEmptyConfigFile"
- >
- {{ $options.i18n.btnText }}
- </gl-button>
+ <div>
+ <pipeline-editor-file-nav v-if="showFileNav" v-on="$listeners" />
+ <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <img :src="emptyStateIllustrationPath" />
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <p class="gl-mt-3">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button
+ v-if="showCTAButton"
+ variant="confirm"
+ class="gl-mt-3"
+ @click="createEmptyConfigFile"
+ >
+ {{ $options.i18n.btnText }}
+ </gl-button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
new file mode 100644
index 00000000000..091b202e10b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
@@ -0,0 +1,155 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
+import {
+ COMMIT_FAILURE,
+ COMMIT_SUCCESS,
+ DEFAULT_FAILURE,
+ DEFAULT_SUCCESS,
+ LOAD_FAILURE_UNKNOWN,
+} from '../../constants';
+import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
+import {
+ CODE_SNIPPET_SOURCE_URL_PARAM,
+ CODE_SNIPPET_SOURCES,
+} from '../code_snippet_alert/constants';
+
+export default {
+ components: {
+ GlAlert,
+ CodeSnippetAlert,
+ },
+ errorTexts: {
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ [DEFAULT_FAILURE]: __('Something went wrong on our end.'),
+ [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ },
+ successTexts: {
+ [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
+ [DEFAULT_SUCCESS]: __('Your action succeeded.'),
+ },
+ props: {
+ failureType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ failureReasons: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showFailure: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showSuccess: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ successType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ codeSnippetCopiedFrom: '',
+ };
+ },
+ computed: {
+ failure() {
+ switch (this.failureType) {
+ 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',
+ };
+ }
+ },
+ success() {
+ switch (this.successType) {
+ case COMMIT_SUCCESS:
+ return {
+ text: this.$options.successTexts[COMMIT_SUCCESS],
+ variant: 'info',
+ };
+ default:
+ return {
+ text: this.$options.successTexts[DEFAULT_SUCCESS],
+ variant: 'info',
+ };
+ }
+ },
+ },
+ created() {
+ this.parseCodeSnippetSourceParam();
+ },
+ methods: {
+ dismissCodeSnippetAlert() {
+ this.codeSnippetCopiedFrom = '';
+ },
+ dismissFailure() {
+ this.$emit('hide-failure');
+ },
+ dismissSuccess() {
+ this.$emit('hide-success');
+ },
+ parseCodeSnippetSourceParam() {
+ const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
+ if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
+ this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
+ window.history.replaceState(
+ {},
+ document.title,
+ removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
+ );
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <code-snippet-alert
+ v-if="codeSnippetCopiedFrom"
+ :source="codeSnippetCopiedFrom"
+ class="gl-mb-5"
+ @dismiss="dismissCodeSnippetAlert"
+ />
+ <gl-alert
+ v-if="showSuccess"
+ :variant="success.variant"
+ class="gl-mb-5"
+ @dismiss="dismissSuccess"
+ >
+ {{ success.text }}
+ </gl-alert>
+ <gl-alert
+ v-if="showFailure"
+ :variant="failure.variant"
+ class="gl-mb-5"
+ @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>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 8d0ec6c3e2d..f0a24e0c061 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -14,6 +14,7 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const CREATE_TAB = 'CREATE_TAB';
@@ -25,3 +26,8 @@ export const TABS_WITH_COMMIT_FORM = [CREATE_TAB, LINT_TAB, VISUALIZE_TAB];
export const COMMIT_ACTION_CREATE = 'CREATE';
export const COMMIT_ACTION_UPDATE = 'UPDATE';
+
+export const DRAWER_EXPANDED_KEY = 'pipeline_editor_drawer_expanded';
+
+export const BRANCH_PAGINATION_LIMIT = 20;
+export const BRANCH_SEARCH_DEBOUNCE = '500';
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
index 3b2daa45a18..94e6facabfd 100644
--- 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
@@ -22,6 +22,7 @@ mutation commitCIFile(
commit {
sha
}
+ commitPipelinePath
errors
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql
index f162bb11d47..46e9b108b41 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.graphql
@@ -1,9 +1,12 @@
-query getAvailableBranches($projectFullPath: ID!) {
- project(fullPath: $projectFullPath) @client {
+query getAvailableBranches(
+ $limit: Int!
+ $offset: Int!
+ $projectFullPath: ID!
+ $searchPattern: String!
+) {
+ project(fullPath: $projectFullPath) {
repository {
- branches {
- name
- }
+ branchNames(limit: $limit, offset: $offset, searchPattern: $searchPattern)
}
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql
new file mode 100644
index 00000000000..b9946a9e233
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.graphql
@@ -0,0 +1,3 @@
+query getPipelineEtag {
+ pipelineEtag @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index caa2a65d424..81e75c32846 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -11,23 +11,6 @@ export const resolvers = {
}),
};
},
- /* eslint-disable @gitlab/require-i18n-strings */
- project() {
- return {
- __typename: 'Project',
- repository: {
- __typename: 'Repository',
- branches: [
- { __typename: 'Branch', name: 'master' },
- { __typename: 'Branch', name: 'main' },
- { __typename: 'Branch', name: 'develop' },
- { __typename: 'Branch', name: 'production' },
- { __typename: 'Branch', name: 'test' },
- ],
- },
- };
- },
- /* eslint-enable @gitlab/require-i18n-strings */
},
Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => {
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 8a1e26f9bff..66158bdba88 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -6,6 +6,7 @@ import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
+import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
@@ -26,15 +27,23 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
// Add to apollo cache as it can be updated by future queries
commitSha,
initialBranchName,
+ pipelineEtag,
// Add to provide/inject API for static values
ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
defaultBranch,
emptyStateIllustrationPath,
+ helpPaths,
lintHelpPagePath,
+ needsHelpPagePath,
newMergeRequestPath,
+ pipelinePagePath,
projectFullPath,
projectPath,
projectNamespace,
+ runnerHelpPagePath,
+ totalBranches,
ymlHelpPagePath,
} = el?.dataset;
@@ -48,7 +57,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, { typeDefs }),
+ defaultClient: createDefaultClient(resolvers, { typeDefs, useGet: true }),
});
const { cache } = apolloProvider.clients.defaultClient;
@@ -66,20 +75,34 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
},
});
+ cache.writeQuery({
+ query: getPipelineEtag,
+ data: {
+ pipelineEtag,
+ },
+ });
+
return new Vue({
el,
apolloProvider,
provide: {
ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
+ configurationPaths,
defaultBranch,
emptyStateIllustrationPath,
+ helpPaths,
lintHelpPagePath,
+ needsHelpPagePath,
newMergeRequestPath,
+ pipelinePagePath,
projectFullPath,
projectPath,
projectNamespace,
+ runnerHelpPagePath,
+ totalBranches: parseInt(totalBranches, 10),
ymlHelpPagePath,
- configurationPaths,
},
render(h) {
return h(PipelineEditorApp);
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index e0fb38004ec..79a2a51cebc 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,21 +1,15 @@
<script>
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
-import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
-import CodeSnippetAlert from './components/code_snippet_alert/code_snippet_alert.vue';
-import {
- CODE_SNIPPET_SOURCE_URL_PARAM,
- CODE_SNIPPET_SOURCES,
-} from './components/code_snippet_alert/constants';
+
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
+import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
- COMMIT_FAILURE,
- COMMIT_SUCCESS,
- DEFAULT_FAILURE,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
@@ -31,11 +25,10 @@ import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
ConfirmUnsavedChangesDialog,
- GlAlert,
GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome,
- CodeSnippetAlert,
+ PipelineEditorMessages,
},
inject: {
ciConfigPath: {
@@ -50,20 +43,20 @@ export default {
ciConfigData: {},
failureType: null,
failureReasons: [],
- showStartScreen: false,
- isNewCiConfigFile: false,
initialCiFileContent: '',
+ isNewCiConfigFile: false,
lastCommittedContent: '',
currentCiFileContent: '',
- showFailureAlert: false,
- showSuccessAlert: false,
successType: null,
- codeSnippetCopiedFrom: '',
+ showStartScreen: false,
+ showSuccess: false,
+ showFailure: false,
};
},
apollo: {
initialCiFileContent: {
+ fetchPolicy: fetchPolicies.NETWORK,
query: getBlobContent,
// If it's a brand new file, we don't want to fetch the content.
// Then when the user commits the first time, the query would run
@@ -87,10 +80,21 @@ export default {
this.lastCommittedContent = fileContent;
this.currentCiFileContent = fileContent;
+
+ // make sure to reset the start screen flag during a refetch
+ // e.g. when switching branches
+ if (fileContent.length) {
+ this.showStartScreen = false;
+ }
},
error(error) {
this.handleBlobContentError(error);
},
+ watchLoading(isLoading) {
+ if (isLoading) {
+ this.setAppStatus(EDITOR_APP_STATUS_LOADING);
+ }
+ },
},
ciConfigData: {
query: getCiConfigData,
@@ -145,50 +149,12 @@ export default {
isEmpty() {
return this.currentCiFileContent === '';
},
- failure() {
- switch (this.failureType) {
- 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',
- };
- }
- },
- success() {
- switch (this.successType) {
- case COMMIT_SUCCESS:
- return {
- text: this.$options.successTexts[COMMIT_SUCCESS],
- variant: 'info',
- };
- default:
- return null;
- }
- },
},
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
- errorTexts: {
- [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
- [DEFAULT_FAILURE]: __('Something went wrong on our end.'),
- [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
- },
- successTexts: {
- [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
- },
watch: {
isEmpty(flag) {
if (flag) {
@@ -196,9 +162,6 @@ export default {
}
},
},
- created() {
- this.parseCodeSnippetSourceParam();
- },
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
@@ -216,24 +179,27 @@ export default {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
},
-
- dismissFailure() {
- this.showFailureAlert = false;
+ hideFailure() {
+ this.showFailure = false;
+ },
+ hideSuccess() {
+ this.showSuccess = false;
},
- dismissSuccess() {
- this.showSuccessAlert = false;
+ async refetchContent() {
+ this.$apollo.queries.initialCiFileContent.skip = false;
+ await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showFailureAlert = true;
+ this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
- this.showSuccessAlert = true;
+ this.showSuccess = true;
this.successType = type;
},
resetContent() {
@@ -266,20 +232,6 @@ export default {
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
- parseCodeSnippetSourceParam() {
- const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
- if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
- this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
- window.history.replaceState(
- {},
- document.title,
- removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
- );
- }
- },
- dismissCodeSnippetAlert() {
- this.codeSnippetCopiedFrom = '';
- },
},
};
</script>
@@ -290,33 +242,18 @@ export default {
<pipeline-editor-empty-state
v-else-if="showStartScreen"
@createEmptyConfigFile="setNewEmptyCiConfigFile"
+ @refetchContent="refetchContent"
/>
<div v-else>
- <code-snippet-alert
- v-if="codeSnippetCopiedFrom"
- :source="codeSnippetCopiedFrom"
- class="gl-mb-5"
- @dismiss="dismissCodeSnippetAlert"
+ <pipeline-editor-messages
+ :failure-type="failureType"
+ :failure-reasons="failureReasons"
+ :show-failure="showFailure"
+ :show-success="showSuccess"
+ :success-type="successType"
+ @hide-success="hideSuccess"
+ @hide-failure="hideFailure"
/>
- <gl-alert
- v-if="showSuccessAlert"
- :variant="success.variant"
- class="gl-mb-5"
- @dismiss="dismissSuccess"
- >
- {{ success.text }}
- </gl-alert>
- <gl-alert
- v-if="showFailureAlert"
- :variant="failure.variant"
- class="gl-mb-5"
- @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>
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
@@ -324,6 +261,7 @@ export default {
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
+ @refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index adba55f9f4b..dfe9c82b912 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -1,5 +1,7 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CommitSection from './components/commit/commit_section.vue';
+import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
@@ -8,10 +10,12 @@ import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
export default {
components: {
CommitSection,
+ PipelineEditorDrawer,
PipelineEditorFileNav,
PipelineEditorHeader,
PipelineEditorTabs,
},
+ mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -35,6 +39,9 @@ export default {
showCommitForm() {
return TABS_WITH_COMMIT_FORM.includes(this.currentTab);
},
+ showPipelineDrawer() {
+ return this.glFeatures.pipelineEditorDrawer;
+ },
},
methods: {
setCurrentTab(tabName) {
@@ -45,7 +52,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-pr-9 gl-transition-medium gl-w-full">
<pipeline-editor-file-nav v-on="$listeners" />
<pipeline-editor-header
:ci-config-data="ciConfigData"
@@ -58,5 +65,6 @@ export default {
@set-current-tab="setCurrentTab"
/>
<commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
+ <pipeline-editor-drawer v-if="showPipelineDrawer" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index e44d80ee9d1..5472e51445a 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -21,7 +21,13 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
-import { VARIABLE_TYPE, FILE_TYPE, CONFIG_VARIABLES_TIMEOUT } from '../constants';
+import {
+ VARIABLE_TYPE,
+ FILE_TYPE,
+ CONFIG_VARIABLES_TIMEOUT,
+ CC_VALIDATION_REQUIRED_ERROR,
+} from '../constants';
+import filterVariables from '../utils/filter_variables';
import RefsDropdown from './refs_dropdown.vue';
const i18n = {
@@ -59,6 +65,8 @@ export default {
GlSprintf,
GlLoadingIcon,
RefsDropdown,
+ CcValidationRequiredAlert: () =>
+ import('ee_component/billings/components/cc_validation_required_alert.vue'),
},
directives: { SafeHtml },
props: {
@@ -142,6 +150,9 @@ export default {
descriptions() {
return this.form[this.refFullName]?.descriptions ?? {};
},
+ ccRequiredError() {
+ return this.error === CC_VALIDATION_REQUIRED_ERROR;
+ },
},
watch: {
refValue() {
@@ -281,20 +292,13 @@ export default {
},
createPipeline() {
this.submitted = true;
- const filteredVariables = this.variables
- .filter(({ key, value }) => key !== '' && value !== '')
- .map(({ variable_type, key, value }) => ({
- variable_type,
- key,
- secret_value: value,
- }));
return axios
.post(this.pipelinesPath, {
// send shortName as fall back for query params
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
ref: this.refValue.fullName || this.refShortName,
- variables_attributes: filteredVariables,
+ variables_attributes: filterVariables(this.variables),
})
.then(({ data }) => {
redirectTo(`${this.pipelinesPath}/${data.id}`);
@@ -335,8 +339,9 @@ export default {
<template>
<gl-form @submit.prevent="createPipeline">
+ <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" />
<gl-alert
- v-if="error"
+ v-else-if="error"
:title="errorTitle"
:dismissible="false"
variant="danger"
@@ -393,6 +398,7 @@ export default {
v-model="variable.variable_type"
:class="$options.formElementClasses"
:options="$options.typeOptions"
+ data-testid="pipeline-form-ci-variable-type"
/>
<gl-form-input
v-model="variable.key"
diff --git a/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
index ed5c659d1df..d35d2010150 100644
--- a/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
+++ b/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
@@ -81,11 +81,12 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="refShortName" block @show.once="loadRefs">
+ <gl-dropdown :text="refShortName" block data-testid="ref-select" @show.once="loadRefs">
<gl-search-box-by-type
v-model.trim="searchTerm"
:is-loading="isLoading"
:placeholder="__('Search refs')"
+ data-testid="search-refs"
/>
<gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
<gl-dropdown-item
diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js
index 681755dc6ab..91a064a0fb8 100644
--- a/app/assets/javascripts/pipeline_new/constants.js
+++ b/app/assets/javascripts/pipeline_new/constants.js
@@ -4,3 +4,6 @@ export const DEBOUNCE_REFS_SEARCH_MS = 250;
export const CONFIG_VARIABLES_TIMEOUT = 5000;
export const BRANCH_REF_TYPE = 'branch';
export const TAG_REF_TYPE = 'tag';
+
+export const CC_VALIDATION_REQUIRED_ERROR =
+ 'Credit card required to be on file in order to create a pipeline';
diff --git a/app/assets/javascripts/pipeline_new/utils/filter_variables.js b/app/assets/javascripts/pipeline_new/utils/filter_variables.js
new file mode 100644
index 00000000000..57ce3d13a9a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/utils/filter_variables.js
@@ -0,0 +1,13 @@
+// We need to filter out blank variables
+// and filter out variables that have no key
+// before sending to the API to create a pipeline.
+
+export default (variables) => {
+ return variables
+ .filter(({ key }) => key !== '')
+ .map(({ variable_type, key, value }) => ({
+ variable_type,
+ key,
+ secret_value: value,
+ }));
+};
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 63048777724..71ec81b8969 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -2,6 +2,7 @@
import { reportToSentry } from '../../utils';
import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
import LinksLayer from '../graph_shared/links_layer.vue';
+import { generateColumnsFromLayersListMemoized } from '../parsing_utils';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
@@ -25,6 +26,10 @@ export default {
type: Object,
required: true,
},
+ showLinks: {
+ type: Boolean,
+ required: true,
+ },
viewType: {
type: String,
required: true,
@@ -74,7 +79,9 @@ export default {
return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
},
layout() {
- return this.isStageView ? this.pipeline.stages : this.generateColumnsFromLayersList();
+ return this.isStageView
+ ? this.pipeline.stages
+ : generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers);
},
hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0);
@@ -91,8 +98,8 @@ export default {
collectMetrics: true,
};
},
- shouldHideLinks() {
- return this.isStageView;
+ showJobLinks() {
+ return !this.isStageView && this.showLinks;
},
shouldShowStageName() {
return !this.isStageView;
@@ -120,26 +127,6 @@ export default {
this.getMeasurements();
},
methods: {
- generateColumnsFromLayersList() {
- return this.pipelineLayers.map((layers, idx) => {
- /*
- look up the groups in each layer,
- then add each set of layer groups to a stage-like object
- */
-
- const groups = layers.map((id) => {
- const { stageIdx, groupIdx } = this.pipeline.stagesLookup[id];
- return this.pipeline.stages?.[stageIdx]?.groups?.[groupIdx];
- });
-
- return {
- name: '',
- id: `layer-${idx}`,
- status: { action: null },
- groups: groups.filter(Boolean),
- };
- });
- },
getMeasurements() {
this.measurements = {
width: this.$refs[this.containerId].scrollWidth,
@@ -178,7 +165,7 @@ export default {
<div class="js-pipeline-graph">
<div
ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap gl-border-t-solid gl-border-t-1 gl-border-gray-100"
:class="{ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline }"
>
<linked-graph-wrapper>
@@ -188,6 +175,7 @@ export default {
:config-paths="configPaths"
:linked-pipelines="upstreamPipelines"
:column-title="__('Upstream')"
+ :show-links="showJobLinks"
:type="$options.pipelineTypeConstants.UPSTREAM"
:view-type="viewType"
@error="onError"
@@ -202,9 +190,8 @@ export default {
:container-measurements="measurements"
:highlighted-job="hoveredJobName"
:metrics-config="metricsConfig"
- :never-show-links="shouldHideLinks"
+ :show-links="showJobLinks"
:view-type="viewType"
- default-link-color="gl-stroke-transparent"
@error="onError"
@highlightedJobsChange="updateHighlightedJobs"
>
@@ -234,6 +221,7 @@ export default {
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
+ :show-links="showJobLinks"
:type="$options.pipelineTypeConstants.DOWNSTREAM"
:view-type="viewType"
@downstreamHovered="setSourceJob"
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 0bc6d883245..9329a35ba99 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -5,7 +5,9 @@ import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
-import { reportToSentry } from '../../utils';
+import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
+import getUserCallouts from '../../graphql/queries/get_user_callouts.query.graphql';
+import { reportToSentry, reportMessageToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
@@ -17,6 +19,9 @@ import {
unwrapPipelineData,
} from './utils';
+const featureName = 'pipeline_needs_hover_tip';
+const enumFeatureName = featureName.toUpperCase();
+
export default {
name: 'PipelineGraphWrapper',
components: {
@@ -44,10 +49,12 @@ export default {
data() {
return {
alertType: null,
+ callouts: [],
currentViewType: STAGE_VIEW,
pipeline: null,
pipelineLayers: null,
showAlert: false,
+ showLinks: false,
};
},
errorTexts: {
@@ -59,6 +66,18 @@ export default {
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
},
apollo: {
+ callouts: {
+ query: getUserCallouts,
+ update(data) {
+ return data?.currentUser?.callouts?.nodes.map((callout) => callout.featureName) || [];
+ },
+ error(err) {
+ reportToSentry(
+ this.$options.name,
+ `type: callout_load_failure, info: ${serializeLoadErrors(err)}`,
+ );
+ },
+ },
pipeline: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
@@ -90,9 +109,16 @@ export default {
},
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
- reportToSentry(
+
+ reportMessageToSentry(
this.$options.name,
- `type: ${LOAD_FAILURE}, info: ${serializeLoadErrors(err)}`,
+ `| type: ${LOAD_FAILURE} , info: ${serializeLoadErrors(err)}`,
+ {
+ projectPath: this.projectPath,
+ pipelineIid: this.pipelineIid,
+ pipelineStages: this.pipeline?.stages?.length || 0,
+ nbOfDownstreams: this.pipeline?.downstream?.length || 0,
+ },
);
},
result({ error }) {
@@ -137,6 +163,13 @@ export default {
metricsPath: this.metricsPath,
};
},
+ graphViewType() {
+ /* This prevents reading view type off the localStorage value if it does not apply. */
+ return this.showGraphViewSelector ? this.currentViewType : STAGE_VIEW;
+ },
+ hoverTipPreviouslyDismissed() {
+ return this.callouts.includes(enumFeatureName);
+ },
showLoadingIcon() {
/*
Shows the icon only when the graph is empty, not when it is is
@@ -166,6 +199,18 @@ export default {
return this.pipelineLayers;
},
+ handleTipDismissal() {
+ try {
+ this.$apollo.mutate({
+ mutation: DismissPipelineGraphCallout,
+ variables: {
+ featureName,
+ },
+ });
+ } catch (err) {
+ reportToSentry(this.$options.name, `type: callout_dismiss_failure, info: ${err}`);
+ }
+ },
hideAlert() {
this.showAlert = false;
this.alertType = null;
@@ -182,6 +227,9 @@ export default {
}
},
/* eslint-enable @gitlab/require-i18n-strings */
+ updateShowLinksState(val) {
+ this.showLinks = val;
+ },
updateViewType(type) {
this.currentViewType = type;
},
@@ -201,8 +249,12 @@ export default {
>
<graph-view-selector
v-if="showGraphViewSelector"
- :type="currentViewType"
+ :type="graphViewType"
+ :show-links="showLinks"
+ :tip-previously-dismissed="hoverTipPreviouslyDismissed"
+ @dismissHoverTip="handleTipDismissal"
@updateViewType="updateViewType"
+ @updateShowLinksState="updateShowLinksState"
/>
</local-storage-sync>
<gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
@@ -211,7 +263,8 @@ export default {
:config-paths="configPaths"
:pipeline="pipeline"
:pipeline-layers="getPipelineLayers()"
- :view-type="currentViewType"
+ :show-links="showLinks"
+ :view-type="graphViewType"
@error="reportFailure"
@refreshPipelineGraph="refreshPipelineGraph"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
index f33e6290e37..1435276edd3 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
@@ -1,17 +1,25 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlSegmentedControl, GlToggle } from '@gitlab/ui';
import { __ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
export default {
name: 'GraphViewSelector',
components: {
- GlDropdown,
- GlDropdownItem,
- GlIcon,
- GlSprintf,
+ GlAlert,
+ GlLoadingIcon,
+ GlSegmentedControl,
+ GlToggle,
},
props: {
+ showLinks: {
+ type: Boolean,
+ required: true,
+ },
+ tipPreviouslyDismissed: {
+ type: Boolean,
+ required: true,
+ },
type: {
type: String,
required: true,
@@ -19,67 +27,138 @@ export default {
},
data() {
return {
- currentViewType: STAGE_VIEW,
+ hoverTipDismissed: false,
+ isToggleLoading: false,
+ isSwitcherLoading: false,
+ segmentSelectedType: this.type,
+ showLinksActive: false,
};
},
i18n: {
- labelText: __('Order jobs by'),
+ hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'),
+ linksLabelText: __('Show dependencies'),
+ viewLabelText: __('Group jobs by'),
},
views: {
[STAGE_VIEW]: {
type: STAGE_VIEW,
text: {
primary: __('Stage'),
- secondary: __('View the jobs grouped into stages'),
},
},
[LAYER_VIEW]: {
type: LAYER_VIEW,
text: {
- primary: __('%{codeStart}needs:%{codeEnd} relationships'),
- secondary: __('View what jobs are needed for a job to run'),
+ primary: __('Job dependencies'),
},
},
},
computed: {
- currentDropdownText() {
- return this.$options.views[this.type].text.primary;
+ showLinksToggle() {
+ return this.segmentSelectedType === LAYER_VIEW;
+ },
+ showTip() {
+ return (
+ this.showLinks &&
+ this.showLinksActive &&
+ !this.tipPreviouslyDismissed &&
+ !this.hoverTipDismissed
+ );
+ },
+ viewTypesList() {
+ return Object.keys(this.$options.views).map((key) => {
+ return {
+ value: key,
+ text: this.$options.views[key].text.primary,
+ };
+ });
+ },
+ },
+ watch: {
+ /*
+ How does this reset the loading? As we note in the methods comment below,
+ the loader is set to on before the update work is undertaken (in the parent).
+ Once the work is complete, one of these values will change, since that's the
+ point of the work. When that happens, the related value will update and we are done.
+
+ The bonus for this approach is that it works the same whichever "direction"
+ the work goes in.
+ */
+ showLinks() {
+ this.isToggleLoading = false;
+ },
+ type() {
+ this.isSwitcherLoading = false;
},
},
methods: {
- itemClick(type) {
- this.$emit('updateViewType', type);
+ dismissTip() {
+ this.hoverTipDismissed = true;
+ this.$emit('dismissHoverTip');
+ },
+ /*
+ In both toggle methods, we use setTimeout so that the loading indicator displays,
+ then the work is done to update the DOM. The process is:
+ → user clicks
+ → call stack: set loading to true
+ → render: the loading icon appears on the screen
+ → callback queue: now do the work to calculate the new view / links
+ (note: this work is done in the parent after the event is emitted)
+
+ setTimeout is how we move the work to the callback queue.
+ We can't use nextTick because that is called before the render loop.
+
+ See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
+ */
+ toggleView(type) {
+ this.isSwitcherLoading = true;
+ setTimeout(() => {
+ this.$emit('updateViewType', type);
+ });
+ },
+ toggleShowLinksActive(val) {
+ this.isToggleLoading = true;
+ setTimeout(() => {
+ this.$emit('updateShowLinksState', val);
+ });
},
},
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-center gl-my-4">
- <span>{{ $options.i18n.labelText }}</span>
- <gl-dropdown data-testid="pipeline-view-selector" class="gl-ml-4">
- <template #button-content>
- <gl-sprintf :message="currentDropdownText">
- <template #code="{ content }">
- <code> {{ content }} </code>
- </template>
- </gl-sprintf>
- <gl-icon class="gl-px-2" name="angle-down" :size="16" />
- </template>
- <gl-dropdown-item
- v-for="view in $options.views"
- :key="view.type"
- :secondary-text="view.text.secondary"
- @click="itemClick(view.type)"
- >
- <b>
- <gl-sprintf :message="view.text.primary">
- <template #code="{ content }">
- <code> {{ content }} </code>
- </template>
- </gl-sprintf>
- </b>
- </gl-dropdown-item>
- </gl-dropdown>
+ <div>
+ <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
+ <gl-loading-icon
+ v-if="isSwitcherLoading"
+ data-testid="switcher-loading-state"
+ class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
+ size="lg"
+ />
+ <span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
+ <gl-segmented-control
+ v-model="segmentSelectedType"
+ :options="viewTypesList"
+ :disabled="isSwitcherLoading"
+ data-testid="pipeline-view-selector"
+ class="gl-mx-4"
+ @input="toggleView"
+ />
+
+ <div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
+ <gl-toggle
+ v-model="showLinksActive"
+ data-testid="show-links-toggle"
+ class="gl-mx-4"
+ :label="$options.i18n.linksLabelText"
+ :is-loading="isToggleLoading"
+ label-position="left"
+ @change="toggleShowLinksActive"
+ />
+ </div>
+ </div>
+ <gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip">
+ {{ $options.i18n.hoverTipText }}
+ </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index 6451605a222..b2a3f27e079 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -53,6 +53,7 @@ export default {
};
</script>
<template>
+ <!-- eslint-disable @gitlab/vue-no-data-toggle -->
<div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright">
<button
type="button"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 7f772e35e55..45113ecff41 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -3,7 +3,7 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import { LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils';
import { listByLayers } from '../parsing_utils';
-import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW } from './constants';
+import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
import LinkedPipeline from './linked_pipeline.vue';
import {
getQueryHeaders,
@@ -32,6 +32,10 @@ export default {
type: Array,
required: true,
},
+ showLinks: {
+ type: Boolean,
+ required: true,
+ },
type: {
type: String,
required: true,
@@ -76,6 +80,9 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
+ graphViewType() {
+ return this.currentPipeline?.usesNeeds ? this.viewType : STAGE_VIEW;
+ },
isUpstream() {
return this.type === UPSTREAM;
},
@@ -217,8 +224,9 @@ export default {
:config-paths="configPaths"
:pipeline="currentPipeline"
:pipeline-layers="getPipelineLayers(pipeline.id)"
+ :show-links="showLinks"
:is-linked-pipeline="true"
- :view-type="viewType"
+ :view-type="graphViewType"
/>
</div>
</li>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index fa2f381c8a4..81d59f1ef65 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -160,7 +160,10 @@ export default {
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
css-class-job-name="gl-build-content"
- :class="{ 'gl-opacity-3': isFadedOut(group.name) }"
+ :class="[
+ { 'gl-opacity-3': isFadedOut(group.name) },
+ 'gl-transition-duration-slow gl-transition-timing-function-ease',
+ ]"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
<div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index 373aa6bf9a1..163b3898c28 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -1,3 +1,4 @@
+import { isEmpty } from 'lodash';
import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
@@ -39,15 +40,15 @@ const serializeGqlErr = (gqlError) => {
const serializeLoadErrors = (errors) => {
const { gqlError, graphQLErrors, networkError, message } = errors;
- if (graphQLErrors) {
+ if (!isEmpty(graphQLErrors)) {
return graphQLErrors.map((err) => serializeGqlErr(err)).join('; ');
}
- if (gqlError) {
+ if (!isEmpty(gqlError)) {
return serializeGqlErr(gqlError);
}
- if (networkError) {
+ if (!isEmpty(networkError)) {
return `Network error: ${networkError.message}`;
}
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/api.js b/app/assets/javascripts/pipelines/components/graph_shared/api.js
index 49cd04d11e9..0fe7d9ffda3 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/api.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/api.js
@@ -2,6 +2,11 @@ import axios from '~/lib/utils/axios_utils';
import { reportToSentry } from '../../utils';
export const reportPerformance = (path, stats) => {
+ // FIXME: https://gitlab.com/gitlab-org/gitlab/-/issues/330245
+ if (!path) {
+ return;
+ }
+
axios.post(path, stats).catch((err) => {
reportToSentry('links_inner_perf', `error: ${err}`);
});
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 202498fb188..7c306683305 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -15,6 +15,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
export const generateLinksData = ({ links }, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID);
+
return links.map((link) => {
const path = d3.path();
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
index 0ed5b8a5f09..5c775df7b48 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -1,19 +1,8 @@
<script>
import { isEmpty } from 'lodash';
-import {
- PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
- PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
- PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- PIPELINES_DETAIL_LINK_DURATION,
- PIPELINES_DETAIL_LINKS_TOTAL,
- PIPELINES_DETAIL_LINKS_JOB_RATIO,
-} from '~/performance/constants';
-import { performanceMarkAndMeasure } from '~/performance/utils';
import { DRAW_FAILURE } from '../../constants';
import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils';
import { STAGE_VIEW } from '../graph/constants';
-import { parseData } from '../parsing_utils';
-import { reportPerformance } from './api';
import { generateLinksData } from './drawing_utils';
export default {
@@ -28,6 +17,10 @@ export default {
type: Object,
required: true,
},
+ parsedData: {
+ type: Object,
+ required: true,
+ },
pipelineId: {
type: Number,
required: true,
@@ -36,15 +29,6 @@ export default {
type: Array,
required: true,
},
- totalGroups: {
- type: Number,
- required: true,
- },
- metricsConfig: {
- type: Object,
- required: false,
- default: () => ({}),
- },
defaultLinkColor: {
type: String,
required: false,
@@ -65,13 +49,9 @@ export default {
return {
links: [],
needsObject: null,
- parsedData: {},
};
},
computed: {
- shouldCollectMetrics() {
- return this.metricsConfig.collectMetrics && this.metricsConfig.path;
- },
hasHighlightedJob() {
return Boolean(this.highlightedJob);
},
@@ -115,13 +95,16 @@ export default {
highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs);
},
+ parsedData() {
+ this.calculateLinkData();
+ },
viewType() {
/*
We need to wait a tick so that the layout reflows
before the links refresh.
*/
this.$nextTick(() => {
- this.refreshLinks();
+ this.calculateLinkData();
});
},
},
@@ -129,69 +112,21 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- if (!isEmpty(this.pipelineData)) {
- this.prepareLinkData();
+ if (!isEmpty(this.parsedData)) {
+ this.calculateLinkData();
}
},
methods: {
- beginPerfMeasure() {
- if (this.shouldCollectMetrics) {
- performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
- }
- },
- finishPerfMeasureAndSend() {
- if (this.shouldCollectMetrics) {
- performanceMarkAndMeasure({
- mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
- measures: [
- {
- name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
- },
- ],
- });
- }
-
- window.requestAnimationFrame(() => {
- const duration = window.performance.getEntriesByName(
- PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- )[0]?.duration;
-
- if (!duration) {
- return;
- }
-
- const data = {
- histograms: [
- { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
- { name: PIPELINES_DETAIL_LINKS_TOTAL, value: this.links.length },
- {
- name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
- value: this.links.length / this.totalGroups,
- },
- ],
- };
-
- reportPerformance(this.metricsConfig.path, data);
- });
- },
isLinkHighlighted(linkRef) {
return this.highlightedLinks.includes(linkRef);
},
- prepareLinkData() {
- this.beginPerfMeasure();
+ calculateLinkData() {
try {
- const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
- this.parsedData = parseData(arrayOfJobs);
- this.refreshLinks();
+ this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
} catch (err) {
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
reportToSentry(this.$options.name, err);
}
- this.finishPerfMeasureAndSend();
- },
- refreshLinks() {
- this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
},
getLinkClasses(link) {
return [
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
index 8dbab245f44..81409752621 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -1,5 +1,4 @@
<script>
-import { GlAlert } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __ } from '~/locale';
import {
@@ -19,10 +18,8 @@ import LinksInner from './links_inner.vue';
export default {
name: 'LinksLayer',
components: {
- GlAlert,
LinksInner,
},
- MAX_GROUPS: 200,
props: {
containerMeasurements: {
type: Object,
@@ -37,15 +34,16 @@ export default {
required: false,
default: () => ({}),
},
- neverShowLinks: {
+ showLinks: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
},
data() {
return {
alertDismissed: false,
+ parsedData: {},
showLinksOverride: false,
};
},
@@ -67,43 +65,15 @@ export default {
shouldCollectMetrics() {
return this.metricsConfig.collectMetrics && this.metricsConfig.path;
},
- showAlert() {
- /*
- This is a hard override that allows us to turn off the links without
- needing to remove the component entirely for iteration or based on graph type.
- */
- if (this.neverShowLinks) {
- return false;
- }
-
- return !this.containerZero && !this.showLinkedLayers && !this.alertDismissed;
- },
showLinkedLayers() {
- /*
- This is a hard override that allows us to turn off the links without
- needing to remove the component entirely for iteration or based on graph type.
- */
- if (this.neverShowLinks) {
- return false;
- }
-
- return (
- !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS)
- );
+ return this.showLinks && !this.containerZero;
},
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- /*
- This is code to get metrics for the graph (to observe links performance).
- It is currently here because we want values for links without drawing them.
- It can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/298930
- is closed and functionality is enabled by default.
- */
-
- if (this.neverShowLinks && !isEmpty(this.pipelineData)) {
+ if (!isEmpty(this.pipelineData)) {
window.requestAnimationFrame(() => {
this.prepareLinkData();
});
@@ -151,19 +121,13 @@ export default {
reportPerformance(this.metricsConfig.path, data);
});
},
- dismissAlert() {
- this.alertDismissed = true;
- },
- overrideShowLinks() {
- this.dismissAlert();
- this.showLinksOverride = true;
- },
prepareLinkData() {
this.beginPerfMeasure();
let numLinks;
try {
const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
- numLinks = parseData(arrayOfJobs).links.length;
+ this.parsedData = parseData(arrayOfJobs);
+ numLinks = this.parsedData.links.length;
} catch (err) {
reportToSentry(this.$options.name, err);
}
@@ -176,24 +140,15 @@ export default {
<links-inner
v-if="showLinkedLayers"
:container-measurements="containerMeasurements"
+ :parsed-data="parsedData"
:pipeline-data="pipelineData"
:total-groups="numGroups"
- :metrics-config="metricsConfig"
v-bind="$attrs"
v-on="$listeners"
>
<slot></slot>
</links-inner>
<div v-else>
- <gl-alert
- v-if="showAlert"
- class="gl-ml-4 gl-mb-4"
- :primary-button-text="$options.i18n.showLinksAnyways"
- @primaryAction="overrideShowLinks"
- @dismiss="dismissAlert"
- >
- {{ $options.i18n.tooManyJobs }}
- </gl-alert>
<div class="gl-display-flex gl-relative">
<slot></slot>
</div>
diff --git a/app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue b/app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue
index 6982586ab12..6dff3828a34 100644
--- a/app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue
+++ b/app/assets/javascripts/pipelines/components/notification/pipeline_notification.vue
@@ -2,7 +2,7 @@
import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import DismissPipelineNotification from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
+import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getUserCallouts from '../../graphql/queries/get_user_callouts.query.graphql';
const featureName = 'pipeline_needs_banner';
@@ -55,7 +55,7 @@ export default {
this.dismissedAlert = true;
try {
this.$apollo.mutate({
- mutation: DismissPipelineNotification,
+ mutation: DismissPipelineGraphCallout,
variables: {
featureName,
},
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index f5ab869633b..9d886e0e379 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -1,4 +1,4 @@
-import { uniqWith, isEqual } from 'lodash';
+import { isEqual, memoize, uniqWith } from 'lodash';
import { createSankey } from './dag/drawing_utils';
/*
@@ -170,3 +170,26 @@ export const listByLayers = ({ stages }) => {
return acc;
}, []);
};
+
+export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
+ return pipelineLayers.map((layers, idx) => {
+ /*
+ Look up the groups in each layer,
+ then add each set of layer groups to a stage-like object.
+ */
+
+ const groups = layers.map((id) => {
+ const { stageIdx, groupIdx } = stagesLookup[id];
+ return stages[stageIdx]?.groups?.[groupIdx];
+ });
+
+ return {
+ name: '',
+ id: `layer-${idx}`,
+ status: { action: null },
+ groups: groups.filter(Boolean),
+ };
+ });
+};
+
+export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare);
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index c3bcfcb18fb..e9773f055a7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,6 +1,8 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
-import Experiment from '~/experimentation/components/experiment.vue';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { startCodeQualityWalkthrough, track } from '~/code_quality_walkthrough/utils';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
+import { getExperimentData } from '~/experimentation/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
@@ -12,12 +14,18 @@ export default {
test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`),
btnText: s__('Pipelines|Get started with CI/CD'),
+ codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
+ codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
+ readable, and accessible to contributors, use GitLab CI/CD
+ to analyze your code quality with every push to your project.`),
+ codeQualityBtnText: s__('Pipelines|Add a code quality job'),
noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'),
},
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
- Experiment,
+ GlButton,
+ GitlabExperiment,
PipelinesCiTemplates,
},
props: {
@@ -29,36 +37,82 @@ export default {
type: Boolean,
required: true,
},
+ codeQualityPagePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
+ isPipelineEmptyStateTemplatesExperimentActive() {
+ return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
+ },
+ },
+ mounted() {
+ startCodeQualityWalkthrough();
+ },
+ methods: {
+ trackClick() {
+ track('cta_clicked');
+ },
},
};
</script>
<template>
<div>
- <experiment name="pipeline_empty_state_templates">
+ <gitlab-experiment
+ v-if="isPipelineEmptyStateTemplatesExperimentActive"
+ name="pipeline_empty_state_templates"
+ >
<template #control>
<gl-empty-state
- v-if="canSetCi"
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.btnText"
:primary-button-link="ciHelpPagePath"
/>
+ </template>
+ <template #candidate>
+ <pipelines-ci-templates />
+ </template>
+ </gitlab-experiment>
+ <gitlab-experiment v-else-if="canSetCi" name="code_quality_walkthrough">
+ <template #control>
<gl-empty-state
- v-else
- title=""
+ :title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
- :description="$options.i18n.noCiDescription"
- />
+ :description="$options.i18n.description"
+ >
+ <template #actions>
+ <gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
+ {{ $options.i18n.btnText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
</template>
<template #candidate>
- <pipelines-ci-templates />
+ <gl-empty-state
+ :title="$options.i18n.codeQualityTitle"
+ :svg-path="emptyStateSvgPath"
+ :description="$options.i18n.codeQualityDescription"
+ >
+ <template #actions>
+ <gl-button :href="codeQualityPagePath" variant="confirm" @click="trackClick()">
+ {{ $options.i18n.codeQualityBtnText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
</template>
- </experiment>
+ </gitlab-experiment>
+ <gl-empty-state
+ v-else
+ title=""
+ :svg-path="emptyStateSvgPath"
+ :description="$options.i18n.noCiDescription"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
new file mode 100644
index 00000000000..d7bd2d731b1
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -0,0 +1,115 @@
+<script>
+import {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { __, s__ } from '~/locale';
+
+export const i18n = {
+ artifacts: __('Artifacts'),
+ downloadArtifact: __('Download %{name} artifact'),
+ artifactSectionHeader: __('Download artifacts'),
+ artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
+};
+
+export default {
+ i18n,
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ GlSprintf,
+ },
+ inject: {
+ artifactsEndpoint: {
+ default: '',
+ },
+ artifactsEndpointPlaceholder: {
+ default: '',
+ },
+ },
+ props: {
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ artifacts: [],
+ hasError: false,
+ isLoading: false,
+ };
+ },
+ methods: {
+ fetchArtifacts() {
+ this.isLoading = true;
+ // Replace the placeholder with the ID of the pipeline we are viewing
+ const endpoint = this.artifactsEndpoint.replace(
+ this.artifactsEndpointPlaceholder,
+ this.pipelineId,
+ );
+ return axios
+ .get(endpoint)
+ .then(({ data }) => {
+ this.artifacts = data.artifacts;
+ })
+ .catch(() => {
+ this.hasError = true;
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ v-gl-tooltip
+ :title="$options.i18n.artifacts"
+ :text="$options.i18n.artifacts"
+ :aria-label="$options.i18n.artifacts"
+ icon="ellipsis_v"
+ data-testid="pipeline-multi-actions-dropdown"
+ right
+ lazy
+ text-sr-only
+ no-caret
+ @show.once="fetchArtifacts"
+ >
+ <gl-dropdown-section-header>{{
+ $options.i18n.artifactSectionHeader
+ }}</gl-dropdown-section-header>
+
+ <gl-alert v-if="hasError" variant="danger" :dismissible="false">
+ {{ $options.i18n.artifactsFetchErrorMessage }}
+ </gl-alert>
+
+ <gl-loading-icon v-if="isLoading" />
+
+ <gl-dropdown-item
+ v-for="(artifact, i) in artifacts"
+ :key="i"
+ :href="artifact.path"
+ rel="nofollow"
+ download
+ data-testid="artifact-item"
+ >
+ <gl-sprintf :message="$options.i18n.downloadArtifact">
+ <template #name>{{ artifact.name }}</template>
+ </gl-sprintf>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
index 81eeead2171..85ee44f427d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
@@ -2,7 +2,7 @@
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
-import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
export default {
@@ -16,8 +16,8 @@ export default {
},
components: {
GlButton,
+ PipelineMultiActions,
PipelinesManualActions,
- PipelinesArtifactsComponent,
},
props: {
pipeline: {
@@ -36,14 +36,6 @@ export default {
};
},
computed: {
- displayPipelineActions() {
- return (
- this.pipeline.flags.retryable ||
- this.pipeline.flags.cancelable ||
- this.pipeline.details.manual_actions.length ||
- this.pipeline.details.artifacts.length
- );
- },
actions() {
if (!this.pipeline || !this.pipeline.details) {
return [];
@@ -76,15 +68,10 @@ export default {
</script>
<template>
- <div v-if="displayPipelineActions" class="gl-text-right">
+ <div class="gl-text-right">
<div class="btn-group">
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
- <pipelines-artifacts-component
- v-if="pipeline.details.artifacts.length"
- :artifacts="pipeline.details.artifacts"
- />
-
<gl-button
v-if="pipeline.flags.retryable"
v-gl-tooltip.hover
@@ -114,6 +101,8 @@ export default {
class="js-pipelines-cancel-button"
@click="handleCancelClick"
/>
+
+ <pipeline-multi-actions :pipeline-id="pipeline.id" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index f14a582d731..0218cb2e1b8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -94,6 +94,11 @@ export default {
type: Object,
required: true,
},
+ codeQualityPagePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -331,6 +336,7 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
+ :code-quality-page-path="codeQualityPagePath"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 9c3990f82df..147fff52101 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -1,40 +1,107 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import {
+ GlAlert,
+ GlDropdown,
+ GlDropdownItem,
+ GlLoadingIcon,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { __, s__ } from '~/locale';
+
+export const i18n = {
+ artifacts: __('Artifacts'),
+ downloadArtifact: __('Download %{name} artifact'),
+ artifactSectionHeader: __('Download artifacts'),
+ artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
+ noArtifacts: s__('Pipelines|No artifacts available'),
+};
export default {
+ i18n,
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
+ GlAlert,
GlDropdown,
GlDropdownItem,
+ GlLoadingIcon,
GlSprintf,
},
- translations: {
- artifacts: __('Artifacts'),
- downloadArtifact: __('Download %{name} artifact'),
+ inject: {
+ artifactsEndpoint: {
+ default: '',
+ },
+ artifactsEndpointPlaceholder: {
+ default: '',
+ },
},
props: {
- artifacts: {
- type: Array,
+ pipelineId: {
+ type: Number,
required: true,
},
},
+ data() {
+ return {
+ artifacts: [],
+ hasError: false,
+ isLoading: false,
+ };
+ },
+ computed: {
+ hasArtifacts() {
+ return Boolean(this.artifacts.length);
+ },
+ },
+ methods: {
+ fetchArtifacts() {
+ this.isLoading = true;
+ // Replace the placeholder with the ID of the pipeline we are viewing
+ const endpoint = this.artifactsEndpoint.replace(
+ this.artifactsEndpointPlaceholder,
+ this.pipelineId,
+ );
+ return axios
+ .get(endpoint)
+ .then(({ data }) => {
+ this.artifacts = data.artifacts;
+ })
+ .catch(() => {
+ this.hasError = true;
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
};
</script>
<template>
<gl-dropdown
v-gl-tooltip
class="build-artifacts js-pipeline-dropdown-download"
- :title="$options.translations.artifacts"
- :text="$options.translations.artifacts"
- :aria-label="$options.translations.artifacts"
+ :title="$options.i18n.artifacts"
+ :text="$options.i18n.artifacts"
+ :aria-label="$options.i18n.artifacts"
icon="download"
right
lazy
text-sr-only
+ @show.once="fetchArtifacts"
>
+ <gl-alert v-if="hasError" variant="danger" :dismissible="false">
+ {{ $options.i18n.artifactsFetchErrorMessage }}
+ </gl-alert>
+
+ <gl-loading-icon v-if="isLoading" />
+
+ <gl-alert v-else-if="!hasArtifacts" variant="info" :dismissible="false">
+ {{ $options.i18n.noArtifacts }}
+ </gl-alert>
+
<gl-dropdown-item
v-for="(artifact, i) in artifacts"
:key="i"
@@ -42,7 +109,7 @@ export default {
rel="nofollow"
download
>
- <gl-sprintf :message="$options.translations.downloadArtifact">
+ <gl-sprintf :message="$options.i18n.downloadArtifact">
<template #name>{{ artifact.name }}</template>
</gl-sprintf>
</gl-dropdown-item>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 492c562ec5c..de3f783ac84 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -1,7 +1,8 @@
<script>
import { GlFilteredSearch } from '@gitlab/ui';
import { map } from 'lodash';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
@@ -43,7 +44,7 @@ export default {
title: s__('Pipeline|Trigger author'),
unique: true,
token: PipelineTriggerAuthorToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
projectId: this.projectId,
},
{
@@ -52,7 +53,7 @@ export default {
title: s__('Pipeline|Branch name'),
unique: true,
token: PipelineBranchNameToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
projectId: this.projectId,
disabled: this.selectedTypes.includes(this.$options.tagType),
},
@@ -62,7 +63,7 @@ export default {
title: s__('Pipeline|Tag name'),
unique: true,
token: PipelineTagNameToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
projectId: this.projectId,
disabled: this.selectedTypes.includes(this.$options.branchType),
},
@@ -72,7 +73,7 @@ export default {
title: s__('Pipeline|Status'),
unique: true,
token: PipelineStatusToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
},
];
},
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
index cc3c8d522b3..f56457a4162 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
@@ -1,9 +1,12 @@
<script>
+import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
+import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants';
import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
+ CodeQualityWalkthrough,
CiBadge,
},
props: {
@@ -23,15 +26,37 @@ export default {
isChildView() {
return this.viewType === CHILD_VIEW;
},
+ shouldRenderCodeQualityWalkthrough() {
+ return Object.values(PIPELINE_STATUSES).includes(this.pipelineStatus.group);
+ },
+ codeQualityStep() {
+ const prefix = [PIPELINE_STATUSES.successWithWarnings, PIPELINE_STATUSES.failed].includes(
+ this.pipelineStatus.group,
+ )
+ ? 'failed'
+ : this.pipelineStatus.group;
+ return `${prefix}_pipeline`;
+ },
+ codeQualityBuildPath() {
+ return this.pipeline?.details?.code_quality_build_path;
+ },
},
};
</script>
<template>
- <ci-badge
- :status="pipelineStatus"
- :show-text="!isChildView"
- :icon-classes="'gl-vertical-align-middle!'"
- data-qa-selector="pipeline_commit_status"
- />
+ <div>
+ <ci-badge
+ id="js-code-quality-walkthrough"
+ :status="pipelineStatus"
+ :show-text="!isChildView"
+ :icon-classes="'gl-vertical-align-middle!'"
+ data-qa-selector="pipeline_commit_status"
+ />
+ <code-quality-walkthrough
+ v-if="shouldRenderCodeQualityWalkthrough"
+ :step="codeQualityStep"
+ :link="codeQualityBuildPath"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue b/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
new file mode 100644
index 00000000000..e9f7874d3e4
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+export const i18n = {
+ noTestsButton: s__('TestReports|Learn more about pipeline test reports'),
+ noTestsDescription: s__('TestReports|No test cases were found in the test report.'),
+ noTestsTitle: s__('TestReports|There are no tests to display'),
+ noReportsButton: s__('TestReports|Learn how to upload pipeline test reports'),
+ noReportsDescription: s__(
+ 'TestReports|You can configure your job to use unit test reports, and GitLab displays a report here and in the related merge request.',
+ ),
+ noReportsTitle: s__('TestReports|There are no test reports for this pipeline'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlEmptyState,
+ },
+ inject: {
+ emptyStateImagePath: {
+ default: '',
+ },
+ hasTestReport: {
+ default: false,
+ },
+ },
+ computed: {
+ emptyStateText() {
+ if (this.hasTestReport) {
+ return {
+ button: this.$options.i18n.noTestsButton,
+ description: this.$options.i18n.noTestsDescription,
+ title: this.$options.i18n.noTestsTitle,
+ };
+ }
+ return {
+ button: this.$options.i18n.noReportsButton,
+ description: this.$options.i18n.noReportsDescription,
+ title: this.$options.i18n.noReportsTitle,
+ };
+ },
+ testReportDocPath() {
+ return helpPagePath('ci/unit_test_reports');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="emptyStateText.title"
+ :description="emptyStateText.description"
+ :svg-path="emptyStateImagePath"
+ :primary-button-link="testReportDocPath"
+ :primary-button-text="emptyStateText.button"
+ />
+</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 2edc84e62cb..47e5bb0bde8 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -1,6 +1,6 @@
<script>
-import { GlBadge, GlModal } from '@gitlab/ui';
-import { __, n__, sprintf } from '~/locale';
+import { GlBadge, GlFriendlyWrap, GlLink, GlModal } from '@gitlab/ui';
+import { __, n__, s__, sprintf } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
export default {
@@ -8,6 +8,8 @@ export default {
components: {
CodeBlock,
GlBadge,
+ GlFriendlyWrap,
+ GlLink,
GlModal,
},
props: {
@@ -50,6 +52,7 @@ export default {
duration: __('Execution time'),
history: __('History'),
trace: __('System output'),
+ attachment: s__('TestReports|Attachment'),
},
modalCloseButton: {
text: __('Close'),
@@ -85,6 +88,18 @@ export default {
</div>
</div>
+ <div v-if="testCase.attachment_url" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
+ <strong class="gl-text-right col-sm-3">{{ $options.text.attachment }}</strong>
+ <gl-link
+ class="col-sm-9"
+ :href="testCase.attachment_url"
+ target="_blank"
+ data-testid="test-case-attachment-url"
+ >
+ <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.attachment_url" />
+ </gl-link>
+ </div>
+
<div
v-if="testCase.system_output"
class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index 58d60e2a185..58d072b0005 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
import TestSummaryTable from './test_summary_table.vue';
@@ -8,6 +9,7 @@ import TestSummaryTable from './test_summary_table.vue';
export default {
name: 'TestReports',
components: {
+ EmptyState,
GlLoadingIcon,
TestSuiteTable,
TestSummary,
@@ -83,11 +85,5 @@ export default {
</transition>
</div>
- <div v-else>
- <div class="row gl-mt-3">
- <div class="col-12">
- <p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
- </div>
- </div>
- </div>
+ <empty-state v-else />
</template>
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql b/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
index e4fd55a28be..e8af1db9592 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
+++ b/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
@@ -1,4 +1,4 @@
-mutation DismissPipelineNotification($featureName: String!) {
+mutation DismissPipelineGraphCallout($featureName: String!) {
userCalloutCreate(input: { featureName: $featureName }) {
errors
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index a2bc049c3c7..911f40f4db3 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
import PipelineGraphLegacy from './components/graph/graph_component_legacy.vue';
@@ -63,7 +64,8 @@ const createLegacyPipelinesDetailApp = (mediator) => {
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
- const { blobPath, summaryEndpoint, suiteEndpoint } = el?.dataset || {};
+ const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
+ el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
@@ -76,6 +78,10 @@ const createTestDetails = () => {
components: {
TestReports,
},
+ provide: {
+ emptyStateImagePath,
+ hasTestReport: parseBoolean(hasTestReport),
+ },
store: testReportsStore,
render(createElement) {
return createElement('test-reports');
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index 9ed4365ad75..c892311782c 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -22,6 +22,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
const {
endpoint,
+ artifactsEndpoint,
+ artifactsEndpointPlaceholder,
pipelineScheduleUrl,
emptyStateSvgPath,
errorStateSvgPath,
@@ -35,12 +37,15 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath,
projectId,
params,
+ codeQualityPagePath,
} = el.dataset;
return new Vue({
el,
provide: {
addCiYmlPath,
+ artifactsEndpoint,
+ artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
},
data() {
@@ -70,6 +75,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath,
projectId,
params: JSON.parse(params),
+ codeQualityPagePath,
},
});
},
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 0a6c326fa3d..800a363cada 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -73,3 +73,12 @@ export const reportToSentry = (component, failureType) => {
Sentry.captureException(failureType);
});
};
+
+export const reportMessageToSentry = (component, message, context) => {
+ Sentry.withScope((scope) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ scope.setContext('Vue data', context);
+ scope.setTag('component', component);
+ Sentry.captureMessage(message);
+ });
+};
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index e1e04d63576..7222c2bd908 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -62,6 +62,7 @@ const projectSelect = () => {
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
order_by: 'similarity',
+ simple: true,
},
projectsCallback,
);
diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
index d96d1035ed0..0fd31381ba6 100644
--- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
@@ -65,6 +65,7 @@ export default {
<gl-dropdown-item
v-if="canRevert"
data-testid="revert-link"
+ data-qa-selector="revert_button"
@click="showModal($options.openRevertModal)"
>
{{ s__('ChangeTypeAction|Revert') }}
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index d2fb524489e..f7cfc82db11 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
+import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
export default {
@@ -36,11 +37,46 @@ export default {
type: String,
required: true,
},
+ defaultProject: {
+ type: Object,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ from: {
+ projects: this.projects,
+ selectedProject: this.defaultProject,
+ revision: this.paramsFrom,
+ refsProjectPath: this.refsProjectPath,
+ },
+ to: {
+ selectedProject: this.defaultProject,
+ revision: this.paramsTo,
+ refsProjectPath: this.refsProjectPath,
+ },
+ };
},
methods: {
onSubmit() {
this.$refs.form.submit();
},
+ onSelectProject({ direction, project }) {
+ const refsPath = joinPaths(gon.relative_url_root || '', `/${project.name}`, '/refs');
+ // direction is either 'from' or 'to'
+ this[direction].refsProjectPath = refsPath;
+ this[direction].selectedProject = project;
+ },
+ onSelectRevision({ direction, revision }) {
+ this[direction].revision = revision; // direction is either 'from' or 'to'
+ },
+ onSwapRevision() {
+ [this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
+ },
},
};
</script>
@@ -57,10 +93,15 @@ export default {
class="gl-lg-flex-direction-row gl-lg-display-flex gl-align-items-center compare-revision-cards"
>
<revision-card
- :refs-project-path="refsProjectPath"
+ data-testid="sourceRevisionCard"
+ :refs-project-path="to.refsProjectPath"
revision-text="Source"
params-name="to"
- :params-branch="paramsTo"
+ :params-branch="to.revision"
+ :projects="to.projects"
+ :selected-project="to.selectedProject"
+ @selectProject="onSelectProject"
+ @selectRevision="onSelectRevision"
/>
<div
class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0"
@@ -69,16 +110,24 @@ export default {
...
</div>
<revision-card
- :refs-project-path="refsProjectPath"
+ data-testid="targetRevisionCard"
+ :refs-project-path="from.refsProjectPath"
revision-text="Target"
params-name="from"
- :params-branch="paramsFrom"
+ :params-branch="from.revision"
+ :projects="from.projects"
+ :selected-project="from.selectedProject"
+ @selectProject="onSelectProject"
+ @selectRevision="onSelectRevision"
/>
</div>
<div class="gl-mt-4">
<gl-button category="primary" variant="success" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
+ <gl-button data-testid="swapRevisionsButton" class="btn btn-default" @click="onSwapRevision">
+ {{ s__('CompareRevisions|Swap revisions') }}
+ </gl-button>
<gl-button
v-if="projectMergeRequestPath"
:href="projectMergeRequestPath"
diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
index cb9d8b64b33..ba1e00a2b36 100644
--- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
@@ -1,57 +1,51 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-const SOURCE_PARAM_NAME = 'to';
-
export default {
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
},
- inject: ['projectTo', 'projectsFrom'],
props: {
paramsName: {
type: String,
required: true,
},
+ projects: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ selectedProject: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
searchTerm: '',
- selectedRepo: {},
};
},
computed: {
+ disableRepoDropdown() {
+ return this.projects === null;
+ },
filteredRepos() {
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
- return this?.projectsFrom.filter(({ name }) =>
- name.toLowerCase().includes(lowerCaseSearchTerm),
- );
- },
- isSourceRevision() {
- return this.paramsName === SOURCE_PARAM_NAME;
+ return this?.projects.filter(({ name }) => name.toLowerCase().includes(lowerCaseSearchTerm));
},
inputName() {
return `${this.paramsName}_project_id`;
},
},
- mounted() {
- this.setDefaultRepo();
- },
methods: {
- onClick(repo) {
- this.selectedRepo = repo;
- this.emitTargetProject(repo.name);
- },
- setDefaultRepo() {
- this.selectedRepo = this.projectTo;
+ onClick(project) {
+ this.emitTargetProject(project);
},
- emitTargetProject(name) {
- if (!this.isSourceRevision) {
- this.$emit('changeTargetProject', name);
- }
+ emitTargetProject(project) {
+ this.$emit('selectProject', { direction: this.paramsName, project });
},
},
};
@@ -59,23 +53,23 @@ export default {
<template>
<div>
- <input type="hidden" :name="inputName" :value="selectedRepo.id" />
+ <input type="hidden" :name="inputName" :value="selectedProject.id" />
<gl-dropdown
- :text="selectedRepo.name"
+ :text="selectedProject.name"
:header-text="s__(`CompareRevisions|Select target project`)"
class="gl-w-full gl-font-monospace gl-sm-pr-3"
toggle-class="gl-min-w-0"
- :disabled="isSourceRevision"
+ :disabled="disableRepoDropdown"
>
<template #header>
- <gl-search-box-by-type v-if="!isSourceRevision" v-model.trim="searchTerm" />
+ <gl-search-box-by-type v-if="!disableRepoDropdown" v-model.trim="searchTerm" />
</template>
- <template v-if="!isSourceRevision">
+ <template v-if="!disableRepoDropdown">
<gl-dropdown-item
v-for="repo in filteredRepos"
:key="repo.id"
is-check-item
- :is-checked="selectedRepo.id === repo.id"
+ :is-checked="selectedProject.id === repo.id"
@click="onClick(repo)"
>
{{ repo.name }}
diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue
index 15d24792310..02a329221cc 100644
--- a/app/assets/javascripts/projects/compare/components/revision_card.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_card.vue
@@ -27,17 +27,14 @@ export default {
required: false,
default: null,
},
- },
- data() {
- return {
- selectedRefsProjectPath: this.refsProjectPath,
- };
- },
- methods: {
- onChangeTargetProject(targetProjectName) {
- if (this.paramsName === 'from') {
- this.selectedRefsProjectPath = `/${targetProjectName}/refs`;
- }
+ projects: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ selectedProject: {
+ type: Object,
+ required: true,
},
},
};
@@ -52,13 +49,16 @@ export default {
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
- @changeTargetProject="onChangeTargetProject"
+ :projects="projects"
+ :selected-project="selectedProject"
+ v-on="$listeners"
/>
<revision-dropdown
class="gl-sm-w-half gl-mt-3 gl-sm-mt-0"
- :refs-project-path="selectedRefsProjectPath"
+ :refs-project-path="refsProjectPath"
:params-name="paramsName"
:params-branch="paramsBranch"
+ v-on="$listeners"
/>
</div>
</gl-card>
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
index d0b69344c12..f0b8e73e528 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -56,6 +56,9 @@ export default {
searchTerm: debounce(function debounceSearch() {
this.searchBranchesAndTags();
}, SEARCH_DEBOUNCE_MS),
+ paramsBranch(newBranch) {
+ this.setSelectedRevision(newBranch);
+ },
},
mounted() {
this.fetchBranchesAndTags();
@@ -84,7 +87,7 @@ export default {
this.loading = true;
if (reset) {
- this.selectedRevision = this.getDefaultBranch();
+ this.setSelectedRevision(this.paramsBranch);
}
return axios
@@ -108,10 +111,14 @@ export default {
return this.paramsBranch || EMPTY_DROPDOWN_TEXT;
},
onClick(revision) {
- this.selectedRevision = revision;
+ this.setSelectedRevision(revision);
+ this.$emit('selectRevision', { direction: this.paramsName, revision });
},
onSearchEnter() {
- this.selectedRevision = this.searchTerm;
+ this.setSelectedRevision(this.searchTerm);
+ },
+ setSelectedRevision(revision) {
+ this.selectedRevision = revision || EMPTY_DROPDOWN_TEXT;
},
},
};
@@ -122,7 +129,7 @@ export default {
<input type="hidden" :name="paramsName" :value="selectedRevision" />
<gl-dropdown
class="gl-w-full gl-font-monospace"
- toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0"
+ toggle-class="form-control compare-dropdown-toggle gl-min-w-0"
:text="selectedRevision"
:header-text="s__('CompareRevisions|Select Git revision')"
:loading="loading"
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
index f57a8942a77..19cf4cda2be 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
@@ -112,7 +112,7 @@ export default {
<input type="hidden" :name="paramsName" :value="selectedRevision" />
<gl-dropdown
class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
- toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
+ toggle-class="form-control compare-dropdown-toggle gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
:text="selectedRevision"
header-text="Select Git revision"
:loading="loading"
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
index 4ba4e308cd4..322dff773b8 100644
--- a/app/assets/javascripts/projects/compare/index.js
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -22,10 +22,6 @@ export default function init() {
components: {
CompareApp,
},
- provide: {
- projectTo: JSON.parse(projectTo),
- projectsFrom: JSON.parse(projectsFrom),
- },
render(createElement) {
return createElement(CompareApp, {
props: {
@@ -35,6 +31,8 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
+ defaultProject: JSON.parse(projectTo),
+ projects: JSON.parse(projectsFrom),
},
});
},
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
deleted file mode 100644
index 1060b37067e..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ /dev/null
@@ -1,201 +0,0 @@
-<script>
-/* eslint-disable vue/no-v-html */
-import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import { experiment } from '~/experimentation/utils';
-import { __, s__ } from '~/locale';
-import { NEW_REPO_EXPERIMENT } from '../constants';
-import blankProjectIllustration from '../illustrations/blank-project.svg';
-import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
-import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
-import importProjectIllustration from '../illustrations/import-project.svg';
-import LegacyContainer from './legacy_container.vue';
-import WelcomePage from './welcome.vue';
-
-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 = [
- {
- key: 'blank',
- name: BLANK_PANEL,
- selector: '#blank-project-pane',
- title: s__('ProjectsNew|Create blank project'),
- description: s__(
- 'ProjectsNew|Create a blank project to house your files, plan your work, and collaborate on code, among other things.',
- ),
- illustration: blankProjectIllustration,
- },
- {
- key: 'template',
- name: 'create_from_template',
- selector: '#create-from-template-pane',
- title: s__('ProjectsNew|Create from template'),
- description: s__(
- 'Create a project pre-populated with the necessary files to get you started quickly.',
- ),
- illustration: createFromTemplateIllustration,
- },
- {
- key: 'import',
- name: 'import_project',
- selector: '#import-project-pane',
- title: s__('ProjectsNew|Import project'),
- description: s__(
- 'Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.',
- ),
- illustration: importProjectIllustration,
- },
- {
- key: 'ci',
- name: CI_CD_PANEL,
- selector: '#ci-cd-project-pane',
- title: s__('ProjectsNew|Run CI/CD for external repository'),
- description: s__('ProjectsNew|Connect your external repository to GitLab CI/CD.'),
- illustration: ciCdProjectIllustration,
- },
-];
-
-export default {
- components: {
- GlBreadcrumb,
- GlIcon,
- WelcomePage,
- LegacyContainer,
- },
- directives: {
- SafeHtml,
- },
- props: {
- hasErrors: {
- type: Boolean,
- required: false,
- default: false,
- },
- isCiCdAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- newProjectGuidelines: {
- type: String,
- required: false,
- default: '',
- },
- },
-
- data() {
- return {
- activeTab: null,
- };
- },
-
- computed: {
- decoratedPanels() {
- const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
- use: () => ({
- blank: s__('ProjectsNew|Create blank project'),
- import: s__('ProjectsNew|Import project'),
- }),
- try: () => ({
- blank: s__('ProjectsNew|Create blank project/repository'),
- import: s__('ProjectsNew|Import project/repository'),
- }),
- });
-
- return PANELS.map(({ key, title, ...el }) => ({
- ...el,
- title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
- }));
- },
-
- availablePanels() {
- if (this.isCiCdAvailable) {
- return this.decoratedPanels;
- }
-
- return this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
- },
-
- activePanel() {
- return this.decoratedPanels.find((p) => p.name === this.activeTab);
- },
-
- breadcrumbs() {
- if (!this.activeTab || !this.activePanel) {
- return null;
- }
-
- return [
- { text: __('New project'), href: '#' },
- { text: this.activePanel.title, href: `#${this.activeTab}` },
- ];
- },
- },
-
- created() {
- this.handleLocationHashChange();
-
- if (this.hasErrors) {
- this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL;
- }
-
- window.addEventListener('hashchange', () => {
- this.handleLocationHashChange();
- this.resetProjectErrors();
- });
- this.$root.$on('clicked::link', (e) => {
- window.location = e.target.href;
- });
- },
-
- methods: {
- resetProjectErrors() {
- const errorsContainer = document.querySelector('.project-edit-errors');
- if (errorsContainer) {
- errorsContainer.innerHTML = '';
- }
- },
-
- handleLocationHashChange() {
- this.activeTab = window.location.hash.substring(1) || null;
- if (this.activeTab) {
- localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab);
- }
- },
- },
-
- PANELS,
-};
-</script>
-
-<template>
- <welcome-page v-if="activeTab === null" :panels="availablePanels" />
- <div v-else class="row">
- <div class="col-lg-3">
- <div class="gl-text-white" v-html="activePanel.illustration"></div>
- <h4>{{ activePanel.title }}</h4>
- <p>{{ activePanel.description }}</p>
- <div
- v-if="newProjectGuidelines"
- id="new-project-guideline"
- v-safe-html="newProjectGuidelines"
- ></div>
- </div>
- <div class="col-lg-9">
- <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
- <template #separator>
- <gl-icon name="chevron-right" :size="8" />
- </template>
- </gl-breadcrumb>
- <template v-for="panel in $options.PANELS">
- <legacy-container
- v-if="activeTab === panel.name"
- :key="panel.name"
- class="gl-mt-3"
- :selector="panel.selector"
- />
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
deleted file mode 100644
index d342ce4c9c2..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-/* eslint-disable vue/no-v-html */
-import Tracking from '~/tracking';
-import { NEW_REPO_EXPERIMENT } from '../constants';
-import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
-
-const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
-
-export default {
- components: {
- NewProjectPushTipPopover,
- },
- mixins: [trackingMixin],
- props: {
- panels: {
- type: Array,
- required: true,
- },
- },
-};
-</script>
-<template>
- <div class="container">
- <div class="blank-state-welcome">
- <h2 class="blank-state-welcome-title gl-mt-5! gl-mb-3!">
- {{ s__('ProjectsNew|Create new project') }}
- </h2>
- <p div class="blank-state-text">&nbsp;</p>
- </div>
- <div class="row blank-state-row">
- <a
- v-for="panel in panels"
- :key="panel.name"
- :href="`#${panel.name}`"
- :data-qa-selector="`${panel.name}_link`"
- class="blank-state blank-state-link experiment-new-project-page-blank-state"
- @click="track('click_tab', { label: panel.name })"
- >
- <div class="blank-state-icon gl-text-white" v-html="panel.illustration"></div>
- <div class="blank-state-body gl-pl-4!">
- <h3 class="blank-state-title experiment-new-project-page-blank-state-title">
- {{ panel.title }}
- </h3>
- <p class="blank-state-text">
- {{ panel.description }}
- </p>
- </div>
- </a>
- </div>
- <div class="blank-state-welcome">
- <p>
- {{ __('You can also create a project from the command line.') }}
- <a
- ref="clipTip"
- href="#"
- click.prevent
- class="push-new-project-tip"
- rel="noopener noreferrer"
- >
- {{ __('Show command') }}
- </a>
- <new-project-push-tip-popover :target="() => $refs.clipTip" />
- </p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/constants.js b/app/assets/javascripts/projects/experiment_new_project_creation/constants.js
deleted file mode 100644
index 402ca887cf1..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const NEW_REPO_EXPERIMENT = 'new_repo';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg
deleted file mode 100644
index f73ae70dba8..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M66.1912 8.19118H77.6176C78.2755 8.19118 78.8088 8.72448 78.8088 9.38235V69.6176C78.8088 70.2755 78.2755 70.8088 77.6176 70.8088H66.1912V8.19118Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.38235"/>
-<path d="M22.0517 19.2723L22.0094 10.1001C22.004 8.92546 22.8555 7.92221 24.0153 7.73664L63.3613 1.44139C64.8087 1.2098 66.12 2.32794 66.12 3.79382V75.8717C66.12 77.3323 64.8177 78.449 63.3742 78.2262L24.3037 72.1952C23.1461 72.0165 22.2902 71.023 22.2848 69.8517L22.2428 60.7554" stroke="#DBDBDB" stroke-width="2.38235"/>
-<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.38235"/>
-<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
-<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
-<path d="M22.3125 48V33.3659" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
-<path d="M15 40.3049H30" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/>
-</svg>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg
deleted file mode 100644
index 8d6cf58f196..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-<svg width="169" height="78" viewBox="0 0 169 78" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M115.571 41.5714L147.714 41.5714C158.365 41.5714 167 32.9369 167 22.2857C167 11.6345 158.365 3 147.714 3C137.063 3 128.429 11.6345 128.429 22.2857C128.429 27.3128 130.352 31.8907 133.503 35.3235" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
-<path d="M115.107 41.5714H125.786C133.084 41.5714 139 47.4877 139 54.7857C139 62.0838 133.084 68 125.786 68C118.488 68 112.571 62.0838 112.571 54.7857C112.571 53.039 112.91 51.3715 113.526 49.8453" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
-<path d="M87.5486 37H76.3943C75.6243 37 75 36.3746 75 35.6032C75 34.8318 75.6243 34.2064 76.3943 34.2064H87.5486C88.3187 34.2064 88.9429 34.8318 88.9429 35.6032C88.9429 36.3746 88.3187 37 87.5486 37Z" fill="#FC6D26"/>
-<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="#FC6D26"/>
-<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="white" fill-opacity="0.6"/>
-<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="#6E49CB"/>
-<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="white" fill-opacity="0.8"/>
-<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="#6E49CB"/>
-<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="white" fill-opacity="0.8"/>
-<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="#6E49CB"/>
-<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="white" fill-opacity="0.4"/>
-<path d="M146.262 24.2349L143.048 21.0153C142.767 20.7338 142.282 20.7323 141.983 21.0313L140.394 22.6236C140.1 22.9181 140.088 23.4002 140.378 23.6903L145.344 28.6651C145.841 29.1637 146.666 29.1795 147.166 28.6793L147.866 27.9779L155.864 19.9653C156.171 19.658 156.167 19.1776 155.868 18.8786L154.279 17.2863C153.985 16.9918 153.495 16.9891 153.194 17.2903L146.262 24.2349Z" fill="#FC6D26"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M125.682 56.7113L123.087 59.3221C122.858 59.5529 122.547 59.6825 122.223 59.6824C121.898 59.6824 121.587 59.5526 121.358 59.3218C121.129 59.091 121 58.7779 121 58.4515C121 58.1251 121.129 57.8121 121.358 57.5813L123.087 55.8412L121.358 54.1011C121.129 53.8703 121 53.5573 121 53.2309C121 52.9045 121.129 52.5915 121.358 52.3606C121.587 52.1298 121.898 52.0001 122.223 52C122.547 51.9999 122.858 52.1296 123.087 52.3603L125.682 54.9711C125.911 55.2019 126.04 55.5149 126.04 55.8412C126.04 56.1675 125.911 56.4805 125.682 56.7113ZM131.796 56.7113L129.202 59.3221C129.088 59.4364 128.954 59.527 128.805 59.5888C128.657 59.6506 128.498 59.6824 128.337 59.6824C128.177 59.6824 128.018 59.6505 127.869 59.5886C127.721 59.5268 127.586 59.4361 127.472 59.3218C127.359 59.2075 127.269 59.0718 127.207 58.9225C127.146 58.7732 127.114 58.6131 127.114 58.4515C127.114 58.2899 127.146 58.1299 127.208 57.9806C127.269 57.8313 127.359 57.6956 127.473 57.5813L129.202 55.8412L127.473 54.1011C127.359 53.9868 127.269 53.8512 127.208 53.7018C127.146 53.5525 127.114 53.3925 127.114 53.2309C127.114 53.0693 127.146 52.9092 127.207 52.7599C127.269 52.6106 127.359 52.4749 127.472 52.3606C127.586 52.2463 127.721 52.1556 127.869 52.0938C128.018 52.0319 128.177 52 128.337 52C128.498 52 128.657 52.0318 128.805 52.0936C128.954 52.1554 129.088 52.246 129.202 52.3603L131.796 54.9711C132.026 55.2019 132.154 55.5149 132.154 55.8412C132.154 56.1675 132.026 56.4805 131.796 56.7113Z" fill="#6E49CB"/>
-<path d="M2 26C2 28.415 14.4361 30.3727 29.7769 30.3727C33.7709 30.3727 37.568 30.24 41 30.0011" stroke="#DBDBDB" stroke-width="1.28173"/>
-<path d="M2 50C2 52.415 14.4361 54.3727 29.7769 54.3727C35.6133 54.3727 41.0293 54.0893 45.5 53.6052" stroke="#DBDBDB" stroke-width="1.28173"/>
-<path d="M57.5537 5V22M2 5V68.6673C2 73.1731 20.9696 75.5204 29.7769 75.5204C38.5842 75.5204 57.5537 73.1731 57.5537 68.6673V57" stroke="#DBDBDB" stroke-width="2.56346" stroke-linejoin="round"/>
-<ellipse cx="29.7769" cy="5.64391" rx="27.7769" ry="3.64391" stroke="#DBDBDB" stroke-width="2.56346"/>
-<ellipse cx="55.4286" cy="39.46" rx="17.4286" ry="17.46" stroke="#6E49CB" stroke-width="2.56346"/>
-<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="#6E49CB"/>
-<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="white" fill-opacity="0.9"/>
-<path d="M61.763 38.5893C62.5797 39.0892 62.5797 40.2756 61.763 40.7756L52.951 46.1704C52.0969 46.6933 51 46.0787 51 45.0773L51 34.2875C51 33.2861 52.0969 32.6715 52.951 33.1944L61.763 38.5893Z" fill="#6E49CB"/>
-</svg>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg
deleted file mode 100644
index 2ff4e4969b1..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M68.1765 8.17647H79.6471C80.2968 8.17647 80.8235 8.70319 80.8235 9.35294V69.6471C80.8235 70.2968 80.2968 70.8235 79.6471 70.8235H68.1765V8.17647Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.35294"/>
-<path d="M24.0504 19L24.0093 10.0746C24.0039 8.9145 24.8449 7.92363 25.9905 7.74035L65.393 1.43595C66.8226 1.20721 68.1176 2.31155 68.1176 3.75934V75.903C68.1176 77.3456 66.8314 78.4485 65.4057 78.2284L26.2788 72.1887C25.1356 72.0122 24.2902 71.0309 24.2849 69.8742L24.244 61" stroke="#DBDBDB" stroke-width="2.35294"/>
-<path d="M60.0194 11.1796L30.0195 15.2198C29.4357 15.2984 29 15.7966 29 16.3857V19.1235C29 19.8153 29.594 20.3578 30.283 20.2951L60.283 17.5679C60.889 17.5128 61.3529 17.0047 61.3529 16.3962V12.3455C61.3529 11.6334 60.7252 11.0845 60.0194 11.1796Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
-<path d="M51.1704 29.1021L41.8902 29.8481C41.0202 29.918 40.5266 30.8776 40.9756 31.626L42.6523 34.4205C42.8676 34.7793 43.2573 34.9968 43.6758 34.9916L51.2794 34.8968C51.9233 34.8888 52.4412 34.3645 52.4412 33.7205V30.2748C52.4412 29.5879 51.8551 29.0471 51.1704 29.1021Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
-<path d="M61.2104 70.6341V40.1765C61.2104 39.5267 60.6837 39 60.0339 39H44.9909C44.4469 39 43.9738 39.373 43.8469 39.9019L41.118 51.2721C41.0819 51.4226 41.0148 51.5672 40.923 51.6918C37.1778 56.7763 34.7228 57.4741 29.7135 59.6826C29.2815 59.873 29.0064 60.3064 29.0162 60.7783L29.1309 66.295C29.1428 66.8693 29.5679 67.3511 30.1362 67.4345L59.8631 71.7981C60.5732 71.9024 61.2104 71.3519 61.2104 70.6341Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/>
-<path d="M43.5694 24L36 24.5" stroke="#DBDBDB" stroke-width="1.17647" stroke-linecap="round"/>
-<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.35294"/>
-<circle cx="23" cy="40" r="17" fill="#6E49CB"/>
-<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/>
-<path d="M22.3125 48V33" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
-<path d="M15 41.3148H30" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/>
-</svg>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg
deleted file mode 100644
index 46b4b097bb6..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg
+++ /dev/null
@@ -1,38 +0,0 @@
-<svg width="169" height="84" viewBox="0 0 169 84" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0)">
-<path d="M153.5 74.5714H165.684C166.411 74.5714 167 73.9822 167 73.2554V8.74461C167 8.01779 166.411 7.42859 165.684 7.42859H153.5" stroke="#DBDBDB" stroke-width="2.63203"/>
-<path d="M107.94 57L108.014 72.9062C108.017 73.5536 108.49 74.1026 109.13 74.2008L151.913 80.7674C152.71 80.8897 153.429 80.273 153.429 79.4666V2.54193C153.429 1.73264 152.705 1.11511 151.906 1.24226L108.829 8.09543C108.187 8.19744 107.716 8.7519 107.719 9.4012L107.771 20.5" stroke="#DBDBDB" stroke-width="2.63203"/>
-<path d="M133.539 52.5313L122.91 51.9925M137.311 52.7225L148.969 53.3135" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M132.224 43.9783L124 43.6955M135.998 44.1081L147.665 44.5092" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M148.238 12.3644L131.189 14.604M117.282 16.4529L126.416 15.2311" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M149.032 36.8519L131.839 37.0342M125 37.0852L127.024 37.0852" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M112.038 66.3444L120.582 67.4102M148.266 70.8634L134.595 69.1581M125.025 67.9644L129.468 68.5186" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M114.352 23.3947L116.215 23.2387M129.258 22.147L119.433 22.9693M137.388 21.4665L145.18 20.8143" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M135.832 29.2067L125.981 29.5888M138.724 28.9864L146.537 28.6833" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M115.114 59.5557L128.942 60.8796M133.782 61.3429L145.19 62.4351" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/>
-<path d="M53.4286 42.4286H21.2857C10.6345 42.4286 2.00002 33.7941 2.00002 23.1429C2.00002 12.4917 10.6345 3.85718 21.2857 3.85718C31.9369 3.85718 40.5714 12.4917 40.5714 23.1429C40.5714 28.17 38.648 32.7479 35.4969 36.1807" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
-<path d="M53.0361 42.4286H42.3571C35.0591 42.4286 29.1428 48.3448 29.1428 55.6429C29.1428 62.9409 35.0591 68.8572 42.3571 68.8572C49.6552 68.8572 55.5714 62.9409 55.5714 55.6429C55.5714 53.8962 55.2325 52.2287 54.6169 50.7025" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4286 51.7144C38.4286 50.9254 39.0682 50.2858 39.8572 50.2858H44.1429C44.829 50.2858 45.4022 50.7695 45.5399 51.4146L47.7105 52.6677C48.3938 53.0622 48.6279 53.9359 48.2334 54.6192C47.3183 56.2042 45.5714 59.2248 45.4609 59.4191C45.1836 59.9063 44.7237 60.2858 44.1429 60.2858H39.8572C39.0682 60.2858 38.4286 59.6462 38.4286 58.8572V51.7144ZM39.8572 51.7144H44.1429V58.8572H39.8572L39.8572 51.7144ZM45.5714 56.3727L46.9962 53.9049L45.5714 53.0823V56.3727Z" fill="#FC6D26"/>
-<path d="M25.5984 15.2331C25.8026 14.471 25.3503 13.6877 24.5882 13.4835C23.8261 13.2793 23.0428 13.7315 22.8386 14.4936L18.4017 31.0524C18.1975 31.8145 18.6497 32.5978 19.4118 32.802C20.1739 33.0062 20.9573 32.5539 21.1615 31.7918L25.5984 15.2331Z" fill="#6E49CB"/>
-<path d="M17.2958 17.8469C17.8537 18.4048 17.8537 19.3093 17.2958 19.8672L14.0203 23.1428L17.2958 26.4183C17.8537 26.9762 17.8537 27.8807 17.2958 28.4386C16.738 28.9965 15.8334 28.9965 15.2755 28.4386L10.9898 24.1529C10.4319 23.595 10.4319 22.6905 10.9898 22.1326L15.2755 17.8469C15.8334 17.289 16.738 17.289 17.2958 17.8469Z" fill="#6E49CB"/>
-<path d="M26.7041 17.8469C26.1462 18.4048 26.1462 19.3093 26.7041 19.8672L29.9797 23.1428L26.7041 26.4183C26.1462 26.9762 26.1462 27.8807 26.7041 28.4386C27.262 28.9965 28.1665 28.9965 28.7244 28.4386L33.0101 24.1529C33.568 23.595 33.568 22.6905 33.0101 22.1326L28.7244 17.8469C28.1665 17.289 27.262 17.289 26.7041 17.8469Z" fill="#6E49CB"/>
-<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="#FC6D26"/>
-<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="white" fill-opacity="0.6"/>
-<path d="M70.5713 35.2857L83.4285 35.2857C84.2175 35.2857 84.8571 35.9253 84.8571 36.7143C84.8571 37.5032 84.2175 38.1428 83.4285 38.1428L70.5713 38.1428C69.7824 38.1428 69.1428 37.5032 69.1428 36.7143C69.1428 35.9253 69.7824 35.2857 70.5713 35.2857Z" fill="#FC6D26"/>
-<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="#6E49CB"/>
-<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="white" fill-opacity="0.8"/>
-<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="#6E49CB"/>
-<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="white" fill-opacity="0.6"/>
-<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="#6E49CB"/>
-<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="white" fill-opacity="0.8"/>
-<circle cx="107.714" cy="38.8571" r="17.8571" stroke="#6E49CB" stroke-width="2.63203"/>
-<circle cx="107.714" cy="38.8573" r="13.5714" fill="#6E49CB"/>
-<circle cx="107.714" cy="38.8573" r="13.5714" fill="white" fill-opacity="0.9"/>
-<path d="M111.431 35.0867L115.367 39.0232L111.431 42.9597C111.016 43.3744 110.344 43.3744 109.929 42.9597C109.515 42.545 109.515 41.8727 109.929 41.458L111.302 40.0851H101.123C100.537 40.0851 100.061 39.6097 100.061 39.0232C100.061 38.4367 100.537 37.9613 101.123 37.9613H111.302L109.929 36.5884C109.515 36.1737 109.515 35.5014 109.929 35.0867C110.344 34.672 111.016 34.672 111.431 35.0867Z" fill="#6E49CB"/>
-</g>
-<defs>
-<clipPath id="clip0">
-<rect width="169" height="84" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/index.js b/app/assets/javascripts/projects/experiment_new_project_creation/index.js
deleted file mode 100644
index ea686d4e1e8..00000000000
--- a/app/assets/javascripts/projects/experiment_new_project_creation/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import NewProjectCreationApp from './components/app.vue';
-
-export default function initNewProjectCreation(el, props) {
- const { pushToCreateProjectCommand, workingWithProjectsHelpPath } = el.dataset;
-
- return new Vue({
- el,
- components: {
- NewProjectCreationApp,
- },
- provide: {
- workingWithProjectsHelpPath,
- pushToCreateProjectCommand,
- },
- render(h) {
- return h(NewProjectCreationApp, { props });
- },
- });
-}
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 8d005373508..25bacc1cc4a 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -9,9 +9,8 @@ export default {
GlTab,
PipelineCharts,
DeploymentFrequencyCharts: () =>
- import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
- LeadTimeCharts: () =>
- import('ee_component/projects/pipelines/charts/components/lead_time_charts.vue'),
+ import('ee_component/dora/components/deployment_frequency_charts.vue'),
+ LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
},
inject: {
shouldRenderDoraCharts: {
@@ -29,7 +28,7 @@ export default {
const chartsToShow = ['pipelines'];
if (this.shouldRenderDoraCharts) {
- chartsToShow.push('deployments', 'lead-time');
+ chartsToShow.push('deployment-frequency', 'lead-time');
}
return chartsToShow;
@@ -62,10 +61,10 @@ export default {
<pipeline-charts />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
- <gl-tab :title="__('Deployments')">
+ <gl-tab :title="__('Deployment frequency')">
<deployment-frequency-charts />
</gl-tab>
- <gl-tab :title="__('Lead Time')">
+ <gl-tab :title="__('Lead time')">
<lead-time-charts />
</gl-tab>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index 6a963616224..1c4413bef71 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -4,6 +4,7 @@ import { GlColumnChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
+import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import {
DEFAULT,
CHART_CONTAINER_HEIGHT,
@@ -21,7 +22,6 @@ import {
} from '../constants';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
-import CiCdAnalyticsCharts from './ci_cd_analytics_charts.vue';
import StatisticsList from './statistics_list.vue';
const defaultAnalyticsValues = {
@@ -301,7 +301,7 @@ export default {
<statistics-list v-else :counts="formattedCounts" />
</div>
<div v-if="!loading" class="col-md-6">
- <strong>{{ __('Duration for the last 30 commits') }}</strong>
+ <strong>{{ __('Pipeline durations for the last 30 commits') }}</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index f46068acd68..80ed9a32039 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, n__ } from '~/locale';
+import { sprintf, n__, s__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -23,6 +23,8 @@ import {
ROOT_IMAGE_TOOLTIP,
} from '../../constants/index';
+import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_container_repository_tags_count.query.graphql';
+
export default {
name: 'DetailsHeader',
components: { GlButton, GlIcon, TitleArea, MetadataItem },
@@ -35,60 +37,77 @@ export default {
type: Object,
required: true,
},
- metadataLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
disabled: {
type: Boolean,
default: false,
required: false,
},
},
+ data() {
+ return {
+ containerRepository: {},
+ fetchTagsCount: false,
+ };
+ },
+ apollo: {
+ containerRepository: {
+ query: getContainerRepositoryTagsCountQuery,
+ variables() {
+ return {
+ id: this.image.id,
+ };
+ },
+ },
+ },
computed: {
+ imageDetails() {
+ return { ...this.image, ...this.containerRepository };
+ },
visibilityIcon() {
- return this.image?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
+ return this.imageDetails?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
},
timeAgo() {
- return this.timeFormatted(this.image.updatedAt);
+ return this.timeFormatted(this.imageDetails.updatedAt);
},
updatedText() {
return sprintf(UPDATED_AT, { time: this.timeAgo });
},
tagCountText() {
- return n__('%d tag', '%d tags', this.image.tagsCount);
+ if (this.$apollo.queries.containerRepository.loading) {
+ return s__('ContainerRegistry|-- tags');
+ }
+ return n__('%d tag', '%d tags', this.imageDetails.tagsCount);
},
cleanupTextAndTooltip() {
- if (!this.image.project.containerExpirationPolicy?.enabled) {
+ if (!this.imageDetails.project.containerExpirationPolicy?.enabled) {
return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP };
}
return {
[UNSCHEDULED_STATUS]: {
text: sprintf(CLEANUP_UNSCHEDULED_TEXT, {
- time: this.timeFormatted(this.image.project.containerExpirationPolicy.nextRunAt),
+ time: this.timeFormatted(this.imageDetails.project.containerExpirationPolicy.nextRunAt),
}),
},
[SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP },
[ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP },
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
- }[this.image?.expirationPolicyCleanupStatus];
+ }[this.imageDetails?.expirationPolicyCleanupStatus];
},
deleteButtonDisabled() {
- return this.disabled || !this.image.canDelete;
+ return this.disabled || !this.imageDetails.canDelete;
},
rootImageTooltip() {
- return !this.image.name ? ROOT_IMAGE_TOOLTIP : '';
+ return !this.imageDetails.name ? ROOT_IMAGE_TOOLTIP : '';
},
imageName() {
- return this.image.name || ROOT_IMAGE_TEXT;
+ return this.imageDetails.name || ROOT_IMAGE_TEXT;
},
},
};
</script>
<template>
- <title-area :metadata-loading="metadataLoading">
+ <title-area>
<template #title>
<span data-testid="title">
{{ imageName }}
@@ -124,13 +143,8 @@ export default {
/>
</template>
<template #right-actions>
- <gl-button
- v-if="!metadataLoading"
- variant="danger"
- :disabled="deleteButtonDisabled"
- @click="$emit('delete')"
- >
- {{ __('Delete') }}
+ <gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
+ {{ __('Delete image repository') }}
</gl-button>
</template>
</title-area>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index bc10246614a..3e19a646f53 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -1,19 +1,32 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
+import { GlButton, GlKeysetPagination } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ REMOVE_TAGS_BUTTON_TITLE,
+ TAGS_LIST_TITLE,
+ GRAPHQL_PAGE_SIZE,
+ FETCH_IMAGES_LIST_ERROR_MESSAGE,
+} from '../../constants/index';
+import getContainerRepositoryTagsQuery from '../../graphql/queries/get_container_repository_tags.query.graphql';
+import EmptyState from './empty_state.vue';
import TagsListRow from './tags_list_row.vue';
+import TagsLoader from './tags_loader.vue';
export default {
name: 'TagsList',
components: {
GlButton,
+ GlKeysetPagination,
TagsListRow,
+ EmptyState,
+ TagsLoader,
},
+ inject: ['config'],
props: {
- tags: {
- type: Array,
- required: false,
- default: () => [],
+ id: {
+ type: [Number, String],
+ required: true,
},
isMobile: {
type: Boolean,
@@ -25,17 +38,46 @@ export default {
default: false,
required: false,
},
+ isImageLoading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
},
+ apollo: {
+ containerRepository: {
+ query: getContainerRepositoryTagsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ error() {
+ createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ },
+ },
+ },
data() {
return {
selectedItems: {},
+ containerRepository: {},
};
},
computed: {
+ tags() {
+ return this.containerRepository?.tags?.nodes || [];
+ },
+ tagsPageInfo() {
+ return this.containerRepository?.tags?.pageInfo;
+ },
+ queryVariables() {
+ return {
+ id: joinPaths(this.config.gidPrefix, `${this.id}`),
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ },
hasSelectedItems() {
return this.tags.some((tag) => this.selectedItems[tag.name]);
},
@@ -45,42 +87,93 @@ export default {
multiDeleteButtonIsDisabled() {
return !this.hasSelectedItems || this.disabled;
},
+ showPagination() {
+ return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
+ },
+ hasNoTags() {
+ return this.tags.length === 0;
+ },
+ isLoading() {
+ return this.isImageLoading || this.$apollo.queries.containerRepository.loading;
+ },
},
methods: {
updateSelectedItems(name) {
this.$set(this.selectedItems, name, !this.selectedItems[name]);
},
+ mapTagsToBeDleeted(items) {
+ return this.tags.filter((tag) => items[tag.name]);
+ },
+ fetchNextPage() {
+ this.$apollo.queries.containerRepository.fetchMore({
+ variables: {
+ after: this.tagsPageInfo?.endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ },
+ fetchPreviousPage() {
+ this.$apollo.queries.containerRepository.fetchMore({
+ variables: {
+ first: null,
+ before: this.tagsPageInfo?.startCursor,
+ last: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ },
},
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
- <h5 data-testid="list-title">
- {{ $options.i18n.TAGS_LIST_TITLE }}
- </h5>
+ <tags-loader v-if="isLoading" />
+ <template v-else>
+ <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
+ <template v-else>
+ <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
+ <h5 data-testid="list-title">
+ {{ $options.i18n.TAGS_LIST_TITLE }}
+ </h5>
- <gl-button
- v-if="showMultiDeleteButton"
- :disabled="multiDeleteButtonIsDisabled"
- category="secondary"
- variant="danger"
- @click="$emit('delete', selectedItems)"
- >
- {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
- </gl-button>
- </div>
- <tags-list-row
- v-for="(tag, index) in tags"
- :key="tag.path"
- :tag="tag"
- :first="index === 0"
- :selected="selectedItems[tag.name]"
- :is-mobile="isMobile"
- :disabled="disabled"
- @select="updateSelectedItems(tag.name)"
- @delete="$emit('delete', { [tag.name]: true })"
- />
+ <gl-button
+ v-if="showMultiDeleteButton"
+ :disabled="multiDeleteButtonIsDisabled"
+ category="secondary"
+ variant="danger"
+ @click="$emit('delete', mapTagsToBeDleeted(selectedItems))"
+ >
+ {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
+ </gl-button>
+ </div>
+ <tags-list-row
+ v-for="(tag, index) in tags"
+ :key="tag.path"
+ :tag="tag"
+ :first="index === 0"
+ :selected="selectedItems[tag.name]"
+ :is-mobile="isMobile"
+ :disabled="disabled"
+ @select="updateSelectedItems(tag.name)"
+ @delete="$emit('delete', mapTagsToBeDleeted({ [tag.name]: true }))"
+ />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ :has-next-page="tagsPageInfo.hasNextPage"
+ :has-previous-page="tagsPageInfo.hasPreviousPage"
+ class="gl-mt-3"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
+ </template>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 74027a376a7..45eb2ce51e4 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -50,6 +50,11 @@ export default {
default: false,
required: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
i18n: {
REMOVE_TAG_BUTTON_TITLE,
@@ -92,19 +97,25 @@ export default {
tagLocation() {
return this.tag.path?.replace(`:${this.tag.name}`, '');
},
- invalidTag() {
+ isInvalidTag() {
return !this.tag.digest;
},
+ isCheckboxDisabled() {
+ return this.isInvalidTag || this.disabled;
+ },
+ isDeleteDisabled() {
+ return this.isInvalidTag || this.disabled || !this.tag.canDelete;
+ },
},
};
</script>
<template>
- <list-item v-bind="$attrs" :selected="selected">
+ <list-item v-bind="$attrs" :selected="selected" :disabled="disabled">
<template #left-action>
<gl-form-checkbox
v-if="tag.canDelete"
- :disabled="invalidTag"
+ :disabled="isCheckboxDisabled"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"
@@ -126,10 +137,11 @@ export default {
:title="tag.location"
:text="tag.location"
category="tertiary"
+ :disabled="disabled"
/>
<gl-icon
- v-if="invalidTag"
+ v-if="isInvalidTag"
v-gl-tooltip="{ title: $options.i18n.MISSING_MANIFEST_WARNING_TOOLTIP }"
name="warning"
class="gl-text-orange-500 gl-mb-2 gl-ml-2"
@@ -162,7 +174,7 @@ export default {
</template>
<template #right-action>
<delete-button
- :disabled="!tag.canDelete || invalidTag"
+ :disabled="isDeleteDisabled"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="tag.canDelete"
@@ -172,7 +184,7 @@ export default {
/>
</template>
- <template v-if="!invalidTag" #details-published>
+ <template v-if="!isInvalidTag" #details-published>
<details-row icon="clock" data-testid="published-date-detail">
<gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT">
<template #repositoryPath>
@@ -187,7 +199,7 @@ export default {
</gl-sprintf>
</details-row>
</template>
- <template v-if="!invalidTag" #details-manifest-digest>
+ <template v-if="!isInvalidTag" #details-manifest-digest>
<details-row icon="log" data-testid="manifest-detail">
<gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST">
<template #digest>
@@ -200,10 +212,11 @@ export default {
:text="tag.digest"
category="tertiary"
size="small"
+ :disabled="disabled"
/>
</details-row>
</template>
- <template v-if="!invalidTag" #details-configuration-digest>
+ <template v-if="!isInvalidTag" #details-configuration-digest>
<details-row icon="cloud-gear" data-testid="configuration-detail">
<gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST">
<template #digest>
@@ -216,6 +229,7 @@ export default {
:text="formattedRevision"
category="tertiary"
size="small"
+ :disabled="disabled"
/>
</details-row>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 0373a84b271..930ad01c758 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -78,6 +78,9 @@ export default {
imageName() {
return this.item.name ? this.item.path : `${this.item.path}/ ${ROOT_IMAGE_TEXT}`;
},
+ routerLinkEvent() {
+ return this.deleting ? '' : 'click';
+ },
},
};
</script>
@@ -97,6 +100,7 @@ export default {
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
data-qa-selector="registry_image_content"
+ :event="routerLinkEvent"
:to="{ name: 'details', params: { id } }"
>
{{ imageName }}
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index 7220f9646db..5dcc042a9c4 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -31,7 +31,7 @@ export const CONFIGURATION_DETAILS_ROW_TEST = s__(
);
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
-export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
+export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected tags');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index 3fd019467ac..88c2e667afd 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -1,12 +1,4 @@
-#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-
-query getContainerRepositoryDetails(
- $id: ID!
- $first: Int
- $last: Int
- $after: String
- $before: String
-) {
+query getContainerRepositoryDetails($id: ID!) {
containerRepository(id: $id) {
id
name
@@ -16,25 +8,8 @@ query getContainerRepositoryDetails(
canDelete
createdAt
updatedAt
- tagsCount
expirationPolicyStartedAt
expirationPolicyCleanupStatus
- tags(after: $after, before: $before, first: $first, last: $last) {
- nodes {
- digest
- location
- path
- name
- revision
- shortRevision
- createdAt
- totalSize
- canDelete
- }
- pageInfo {
- ...PageInfo
- }
- }
project {
visibility
containerExpirationPolicy {
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
new file mode 100644
index 00000000000..a703c2dd0ac
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
@@ -0,0 +1,29 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getContainerRepositoryTags(
+ $id: ID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
+ containerRepository(id: $id) {
+ id
+ tags(after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ digest
+ location
+ path
+ name
+ revision
+ shortRevision
+ createdAt
+ totalSize
+ canDelete
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
new file mode 100644
index 00000000000..9092a71edb0
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
@@ -0,0 +1,6 @@
+query getContainerRepositoryTagsCount($id: ID!) {
+ containerRepository(id: $id) {
+ id
+ tagsCount
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 2f515356fa7..34ec3b085a5 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -1,5 +1,5 @@
<script>
-import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
+import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -21,7 +21,6 @@ import {
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
ALERT_DANGER_IMAGE,
- GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
@@ -36,7 +35,6 @@ export default {
DeleteAlert,
PartialCleanupAlert,
DetailsHeader,
- GlKeysetPagination,
DeleteModal,
TagsList,
TagsLoader,
@@ -50,16 +48,12 @@ export default {
mixins: [Tracking.mixin()],
inject: ['breadCrumbState', 'config'],
apollo: {
- image: {
+ containerRepository: {
query: getContainerRepositoryDetailsQuery,
variables() {
return this.queryVariables;
},
- update(data) {
- return data.containerRepository;
- },
- result({ data }) {
- this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
+ result() {
this.updateBreadcrumb();
},
error() {
@@ -69,8 +63,7 @@ export default {
},
data() {
return {
- image: {},
- tagsPageInfo: {},
+ containerRepository: {},
itemsToBeDeleted: [],
isMobile: false,
mutationLoading: false,
@@ -83,19 +76,15 @@ export default {
queryVariables() {
return {
id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`),
- first: GRAPHQL_PAGE_SIZE,
};
},
isLoading() {
- return this.$apollo.queries.image.loading || this.mutationLoading;
- },
- tags() {
- return this.image?.tags?.nodes || [];
+ return this.$apollo.queries.containerRepository.loading || this.mutationLoading;
},
showPartialCleanupWarning() {
return (
this.config.showUnfinishedTagCleanupCallout &&
- this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
+ this.containerRepository?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
!this.hidePartialCleanupWarning
);
},
@@ -105,26 +94,20 @@ export default {
this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
};
},
- showPagination() {
- return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
- },
- hasNoTags() {
- return this.tags.length === 0;
- },
pageActionsAreDisabled() {
- return Boolean(this.image?.status);
+ return Boolean(this.containerRepository?.status);
},
},
methods: {
updateBreadcrumb() {
- const name = this.image?.id
- ? this.image?.name || ROOT_IMAGE_TEXT
+ const name = this.containerRepository?.id
+ ? this.containerRepository?.name || ROOT_IMAGE_TEXT
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
this.breadCrumbState.updateName(name);
},
deleteTags(toBeDeleted) {
this.deleteImageAlert = false;
- this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
+ this.itemsToBeDeleted = toBeDeleted;
this.track('click_button');
this.$refs.deleteModal.show();
},
@@ -170,33 +153,6 @@ export default {
handleResize() {
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
},
- fetchNextPage() {
- if (this.tagsPageInfo?.hasNextPage) {
- this.$apollo.queries.image.fetchMore({
- variables: {
- after: this.tagsPageInfo?.endCursor,
- first: GRAPHQL_PAGE_SIZE,
- },
- updateQuery(previousResult, { fetchMoreResult }) {
- return fetchMoreResult;
- },
- });
- }
- },
- fetchPreviousPage() {
- if (this.tagsPageInfo?.hasPreviousPage) {
- this.$apollo.queries.image.fetchMore({
- variables: {
- first: null,
- before: this.tagsPageInfo?.startCursor,
- last: GRAPHQL_PAGE_SIZE,
- },
- updateQuery(previousResult, { fetchMoreResult }) {
- return fetchMoreResult;
- },
- });
- }
- },
dismissPartialCleanupWarning() {
this.hidePartialCleanupWarning = true;
axios.post(this.config.userCalloutsPath, {
@@ -205,7 +161,7 @@ export default {
},
deleteImage() {
this.deleteImageAlert = true;
- this.itemsToBeDeleted = [{ path: this.image.path }];
+ this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
this.$refs.deleteModal.show();
},
deleteImageError() {
@@ -221,7 +177,7 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
- <template v-if="image">
+ <template v-if="containerRepository">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
@@ -236,40 +192,27 @@ export default {
@dismiss="dismissPartialCleanupWarning"
/>
- <status-alert v-if="image.status" :status="image.status" />
+ <status-alert v-if="containerRepository.status" :status="containerRepository.status" />
<details-header
- :image="image"
- :metadata-loading="isLoading"
+ v-if="!isLoading"
+ :image="containerRepository"
:disabled="pageActionsAreDisabled"
@delete="deleteImage"
/>
<tags-loader v-if="isLoading" />
- <template v-else>
- <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
- <template v-else>
- <tags-list
- :tags="tags"
- :is-mobile="isMobile"
- :disabled="pageActionsAreDisabled"
- @delete="deleteTags"
- />
- <div class="gl-display-flex gl-justify-content-center">
- <gl-keyset-pagination
- v-if="showPagination"
- :has-next-page="tagsPageInfo.hasNextPage"
- :has-previous-page="tagsPageInfo.hasPreviousPage"
- class="gl-mt-3"
- @prev="fetchPreviousPage"
- @next="fetchNextPage"
- />
- </div>
- </template>
- </template>
+ <tags-list
+ v-else
+ :id="$route.params.id"
+ :is-image-loading="isLoading"
+ :is-mobile="isMobile"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteTags"
+ />
<delete-image
- :id="image.id"
+ :id="containerRepository.id"
ref="deleteImage"
use-update-fn
@start="deleteImageIniit"
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 262b5614d65..31d335fa15d 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -51,12 +51,8 @@ export default {
}),
fetchReleases() {
this.fetchReleasesStoreAction({
- // these two parameters are only used in "GraphQL mode"
before: getParameterByName('before'),
after: getParameterByName('after'),
-
- // this parameter is only used when in "REST mode"
- page: getParameterByName('page'),
});
},
},
@@ -73,17 +69,17 @@ export default {
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
category="primary"
variant="success"
- class="js-new-release-btn"
+ data-testid="new-release-button"
>
{{ __('New release') }}
</gl-button>
</div>
- <release-skeleton-loader v-if="isLoading" class="js-loading" />
+ <release-skeleton-loader v-if="isLoading" />
<gl-empty-state
v-else-if="shouldRenderEmptyState"
- class="js-empty-state"
+ data-testid="empty-state"
:title="__('Getting started with releases')"
:svg-path="illustrationPath"
>
@@ -101,7 +97,7 @@ export default {
</template>
</gl-empty-state>
- <div v-else-if="shouldRenderSuccessState" class="js-success-state">
+ <div v-else-if="shouldRenderSuccessState" data-testid="success-state">
<release-block
v-for="(release, index) in releases"
:key="index"
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index c38e93d420b..fdb0f99b735 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,7 +1,7 @@
<script>
import createFlash from '~/flash';
import { s__ } from '~/locale';
-import oneReleaseQuery from '../queries/one_release.query.graphql';
+import oneReleaseQuery from '../graphql/queries/one_release.query.graphql';
import { convertGraphQLRelease } from '../util';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue
index 062c72b445b..fddf85ead1e 100644
--- a/app/assets/javascripts/releases/components/releases_pagination.vue
+++ b/app/assets/javascripts/releases/components/releases_pagination.vue
@@ -1,20 +1,37 @@
<script>
-import { mapGetters } from 'vuex';
-import ReleasesPaginationGraphql from './releases_pagination_graphql.vue';
-import ReleasesPaginationRest from './releases_pagination_rest.vue';
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
- name: 'ReleasesPagination',
- components: { ReleasesPaginationGraphql, ReleasesPaginationRest },
+ name: 'ReleasesPaginationGraphql',
+ components: { GlKeysetPagination },
computed: {
- ...mapGetters(['useGraphQLEndpoint']),
+ ...mapState('index', ['pageInfo']),
+ showPagination() {
+ return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
+ },
+ },
+ methods: {
+ ...mapActions('index', ['fetchReleases']),
+ onPrev(before) {
+ historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
+ this.fetchReleases({ before });
+ },
+ onNext(after) {
+ historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
+ this.fetchReleases({ after });
+ },
},
};
</script>
-
<template>
<div class="gl-display-flex gl-justify-content-center">
- <releases-pagination-graphql v-if="useGraphQLEndpoint" />
- <releases-pagination-rest v-else />
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pageInfo"
+ @prev="onPrev($event)"
+ @next="onNext($event)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
deleted file mode 100644
index 13cbf95b9af..00000000000
--- a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import { GlKeysetPagination } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
-import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
-
-export default {
- name: 'ReleasesPaginationGraphql',
- components: { GlKeysetPagination },
- computed: {
- ...mapState('index', ['graphQlPageInfo']),
- showPagination() {
- return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage;
- },
- },
- methods: {
- ...mapActions('index', ['fetchReleases']),
- onPrev(before) {
- historyPushState(buildUrlWithCurrentLocation(`?before=${before}`));
- this.fetchReleases({ before });
- },
- onNext(after) {
- historyPushState(buildUrlWithCurrentLocation(`?after=${after}`));
- this.fetchReleases({ after });
- },
- },
-};
-</script>
-<template>
- <gl-keyset-pagination
- v-if="showPagination"
- v-bind="graphQlPageInfo"
- @prev="onPrev($event)"
- @next="onNext($event)"
- />
-</template>
diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue
deleted file mode 100644
index 5e97a5a0450..00000000000
--- a/app/assets/javascripts/releases/components/releases_pagination_rest.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-<script>
-import { mapActions, mapState } from 'vuex';
-import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-
-export default {
- name: 'ReleasesPaginationRest',
- components: { TablePagination },
- computed: {
- ...mapState('index', ['restPageInfo']),
- },
- methods: {
- ...mapActions('index', ['fetchReleases']),
- onChangePage(page) {
- historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
- this.fetchReleases({ page });
- },
- },
-};
-</script>
-
-<template>
- <table-pagination :change="onChangePage" :page-info="restPageInfo" />
-</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 9df646ca798..80f59485426 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -74,6 +74,21 @@ export default {
// we need to show the "create from" input.
this.showCreateFrom = true;
},
+ shouldShowCreateTagOption(isLoading, matches, query) {
+ // Show the "create tag" option if:
+ return (
+ // we're not currently loading any results, and
+ !isLoading &&
+ // the search query isn't just whitespace, and
+ query.trim() &&
+ // the `matches` object is non-null, and
+ matches &&
+ // the tag name doesn't already exist
+ !matches.tags.list.some(
+ (tagInfo) => tagInfo.name.toUpperCase() === query.toUpperCase().trim(),
+ )
+ );
+ },
},
translations: {
tagName: {
@@ -111,7 +126,7 @@ export default {
>
<template #footer="{ isLoading, matches, query }">
<gl-dropdown-item
- v-if="!isLoading && matches && matches.tags.totalCount === 0"
+ v-if="shouldShowCreateTagOption(isLoading, matches, query)"
is-check-item
:is-checked="tagName === query"
@click="createTagClicked(query)"
diff --git a/app/assets/javascripts/releases/queries/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
index 3a742db7d9e..3a742db7d9e 100644
--- a/app/assets/javascripts/releases/queries/release.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
new file mode 100644
index 00000000000..47c5afefd78
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
@@ -0,0 +1,23 @@
+fragment ReleaseForEditing on Release {
+ name
+ tagName
+ description
+ assets {
+ links {
+ nodes {
+ id
+ name
+ url
+ linkType
+ }
+ }
+ }
+ links {
+ selfUrl
+ }
+ milestones {
+ nodes {
+ title
+ }
+ }
+}
diff --git a/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql
new file mode 100644
index 00000000000..56bfe7c23d6
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/mutations/create_release.mutation.graphql
@@ -0,0 +1,10 @@
+mutation createRelease($input: ReleaseCreateInput!) {
+ releaseCreate(input: $input) {
+ release {
+ links {
+ selfUrl
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql
new file mode 100644
index 00000000000..4bdfc79dbc4
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/mutations/create_release_link.mutation.graphql
@@ -0,0 +1,5 @@
+mutation createReleaseAssetLink($input: ReleaseAssetLinkCreateInput!) {
+ releaseAssetLinkCreate(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql
new file mode 100644
index 00000000000..a75eddcd288
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/mutations/delete_release_link.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteReleaseAssetLink($input: ReleaseAssetLinkDeleteInput!) {
+ releaseAssetLinkDelete(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql b/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql
new file mode 100644
index 00000000000..9c6a861d2f1
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/mutations/update_release.mutation.graphql
@@ -0,0 +1,5 @@
+mutation updateRelease($input: ReleaseUpdateInput!) {
+ releaseUpdate(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index a07dabb9fd6..10e4d883e62 100644
--- a/app/assets/javascripts/releases/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -1,4 +1,4 @@
-#import "./release.fragment.graphql"
+#import "../fragments/release.fragment.graphql"
query allReleases(
$fullPath: ID!
diff --git a/app/assets/javascripts/releases/queries/one_release.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql
index b893aea94b0..c80d6e753ab 100644
--- a/app/assets/javascripts/releases/queries/one_release.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/one_release.query.graphql
@@ -1,4 +1,4 @@
-#import "./release.fragment.graphql"
+#import "../fragments/release.fragment.graphql"
query oneRelease($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql
new file mode 100644
index 00000000000..767ba4aeca0
--- /dev/null
+++ b/app/assets/javascripts/releases/graphql/queries/one_release_for_editing.query.graphql
@@ -0,0 +1,9 @@
+#import "../fragments/release_for_editing.fragment.graphql"
+
+query oneReleaseForEditing($fullPath: ID!, $tagName: String!) {
+ project(fullPath: $fullPath) {
+ release(tagName: $tagName) {
+ ...ReleaseForEditing
+ }
+ }
+}
diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js
index 0b453467c13..bb21ec7c43f 100644
--- a/app/assets/javascripts/releases/mount_index.js
+++ b/app/assets/javascripts/releases/mount_index.js
@@ -15,11 +15,6 @@ export default () => {
modules: {
index: createIndexModule(el.dataset),
},
- featureFlags: {
- graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData),
- graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage),
- graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats),
- },
}),
render: (h) => h(ReleaseIndexApp),
});
diff --git a/app/assets/javascripts/releases/stores/getters.js b/app/assets/javascripts/releases/stores/getters.js
deleted file mode 100644
index 2a06f398e26..00000000000
--- a/app/assets/javascripts/releases/stores/getters.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * @returns {Boolean} `true` if all the feature flags
- * required to enable the GraphQL endpoint are enabled
- */
-export const useGraphQLEndpoint = (rootState) => {
- return Boolean(
- rootState.featureFlags.graphqlReleaseData &&
- rootState.featureFlags.graphqlReleasesPage &&
- rootState.featureFlags.graphqlMilestoneStats,
- );
-};
diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js
index cc8b586964f..b2e93d789d7 100644
--- a/app/assets/javascripts/releases/stores/index.js
+++ b/app/assets/javascripts/releases/stores/index.js
@@ -1,9 +1,7 @@
import Vuex from 'vuex';
-import * as getters from './getters';
export default ({ modules, featureFlags }) =>
new Vuex.Store({
modules,
state: { featureFlags },
- getters,
});
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 8dc2083dd2b..b312c2a7506 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,14 +1,12 @@
-import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import oneReleaseQuery from '~/releases/queries/one_release.query.graphql';
-import {
- releaseToApiJson,
- apiJsonToRelease,
- gqClient,
- convertOneReleaseGraphQLResponse,
-} from '~/releases/util';
+import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
+import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
+import deleteReleaseAssetLinkMutation from '~/releases/graphql/mutations/delete_release_link.mutation.graphql';
+import updateReleaseMutation from '~/releases/graphql/mutations/update_release.mutation.graphql';
+import oneReleaseForEditingQuery from '~/releases/graphql/queries/one_release_for_editing.query.graphql';
+import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';
import * as types from './mutation_types';
export const initializeRelease = ({ commit, dispatch, getters }) => {
@@ -24,38 +22,25 @@ export const initializeRelease = ({ commit, dispatch, getters }) => {
return Promise.resolve();
};
-export const fetchRelease = ({ commit, state, rootState }) => {
+export const fetchRelease = async ({ commit, state }) => {
commit(types.REQUEST_RELEASE);
- if (rootState.featureFlags?.graphqlIndividualReleasePage) {
- return gqClient
- .query({
- query: oneReleaseQuery,
- variables: {
- fullPath: state.projectPath,
- tagName: state.tagName,
- },
- })
- .then((response) => {
- const { data: release } = convertOneReleaseGraphQLResponse(response);
-
- commit(types.RECEIVE_RELEASE_SUCCESS, release);
- })
- .catch((error) => {
- commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while getting the release details.'));
- });
- }
-
- return api
- .release(state.projectId, state.tagName)
- .then(({ data }) => {
- commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data));
- })
- .catch((error) => {
- commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while getting the release details.'));
+ try {
+ const fetchResponse = await gqClient.query({
+ query: oneReleaseForEditingQuery,
+ variables: {
+ fullPath: state.projectPath,
+ tagName: state.tagName,
+ },
});
+
+ const { data: release } = convertOneReleaseGraphQLResponse(fetchResponse);
+
+ commit(types.RECEIVE_RELEASE_SUCCESS, release);
+ } catch (error) {
+ commit(types.RECEIVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while getting the release details.'));
+ }
};
export const updateReleaseTagName = ({ commit }, tagName) =>
@@ -94,9 +79,9 @@ export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
};
-export const receiveSaveReleaseSuccess = ({ commit }, release) => {
+export const receiveSaveReleaseSuccess = ({ commit }, urlToRedirectTo) => {
commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
- redirectTo(release._links.self);
+ redirectTo(urlToRedirectTo);
};
export const saveRelease = ({ commit, dispatch, getters }) => {
@@ -105,83 +90,130 @@ export const saveRelease = ({ commit, dispatch, getters }) => {
dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease');
};
-export const createRelease = ({ commit, dispatch, state, getters }) => {
- const apiJson = releaseToApiJson(
- {
- ...state.release,
- assets: {
- links: getters.releaseLinksToCreate,
- },
- },
- state.createFrom,
- );
+/**
+ * Tests a GraphQL mutation response for the existence of any errors-as-data
+ * (See https://docs.gitlab.com/ee/development/fe_guide/graphql.html#errors-as-data).
+ * If any errors occurred, throw a JavaScript `Error` object, so that this can be
+ * handled by the global error handler.
+ *
+ * @param {Object} gqlResponse The response object returned by the GraphQL client
+ * @param {String} mutationName The name of the mutation that was executed
+ * @param {String} messageIfError An message to build into the error object if something went wrong
+ */
+const checkForErrorsAsData = (gqlResponse, mutationName, messageIfError) => {
+ const allErrors = gqlResponse.data[mutationName].errors;
+ if (allErrors.length > 0) {
+ const allErrorMessages = JSON.stringify(allErrors);
+ throw new Error(`${messageIfError}: ${allErrorMessages}`);
+ }
+};
- return api
- .createRelease(state.projectId, apiJson)
- .then(({ data }) => {
- dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data));
- })
- .catch((error) => {
- commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while creating a new release'));
+export const createRelease = async ({ commit, dispatch, state, getters }) => {
+ try {
+ const response = await gqClient.mutate({
+ mutation: createReleaseMutation,
+ variables: getters.releaseCreateMutatationVariables,
});
+
+ checkForErrorsAsData(
+ response,
+ 'releaseCreate',
+ `Something went wrong while creating a new release with projectPath "${state.projectPath}" and tagName "${state.release.tagName}"`,
+ );
+
+ dispatch('receiveSaveReleaseSuccess', response.data.releaseCreate.release.links.selfUrl);
+ } catch (error) {
+ commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while creating a new release.'));
+ }
};
-export const updateRelease = ({ commit, dispatch, state, getters }) => {
- const apiJson = releaseToApiJson({
- ...state.release,
- assets: {
- links: getters.releaseLinksToCreate,
+/**
+ * Deletes a single release link.
+ * Throws an error if any network or validation errors occur.
+ */
+const deleteReleaseLinks = async ({ state, id }) => {
+ const deleteResponse = await gqClient.mutate({
+ mutation: deleteReleaseAssetLinkMutation,
+ variables: {
+ input: { id },
},
});
- let updatedRelease = null;
-
- return (
- api
- .updateRelease(state.projectId, state.tagName, apiJson)
-
- /**
- * Currently, we delete all existing links and then
- * recreate new ones on each edit. This is because the
- * REST API doesn't support bulk updating of Release links,
- * and updating individual links can lead to validation
- * race conditions (in particular, the "URLs must be unique")
- * constraint.
- *
- * This isn't ideal since this is no longer an atomic
- * operation - parts of it can fail while others succeed,
- * leaving the Release in an inconsistent state.
- *
- * This logic should be refactored to use GraphQL once
- * https://gitlab.com/gitlab-org/gitlab/-/issues/208702
- * is closed.
- */
- .then(({ data }) => {
- // Save this response since we need it later in the Promise chain
- updatedRelease = data;
-
- // Delete all links currently associated with this Release
- return Promise.all(
- getters.releaseLinksToDelete.map((l) =>
- api.deleteReleaseLink(state.projectId, state.release.tagName, l.id),
- ),
- );
- })
- .then(() => {
- // Create a new link for each link in the form
- return Promise.all(
- apiJson.assets.links.map((l) =>
- api.createReleaseLink(state.projectId, state.release.tagName, l),
- ),
- );
- })
- .then(() => {
- dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease));
- })
- .catch((error) => {
- commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while saving the release details'));
- })
+ checkForErrorsAsData(
+ deleteResponse,
+ 'releaseAssetLinkDelete',
+ `Something went wrong while deleting release asset link for release with projectPath "${state.projectPath}", tagName "${state.tagName}", and link id "${id}"`,
);
};
+
+/**
+ * Creates a single release link.
+ * Throws an error if any network or validation errors occur.
+ */
+const createReleaseLink = async ({ state, link }) => {
+ const createResponse = await gqClient.mutate({
+ mutation: createReleaseAssetLinkMutation,
+ variables: {
+ input: {
+ projectPath: state.projectPath,
+ tagName: state.tagName,
+ name: link.name,
+ url: link.url,
+ linkType: link.linkType.toUpperCase(),
+ },
+ },
+ });
+
+ checkForErrorsAsData(
+ createResponse,
+ 'releaseAssetLinkCreate',
+ `Something went wrong while creating a release asset link for release with projectPath "${state.projectPath}" and tagName "${state.tagName}"`,
+ );
+};
+
+export const updateRelease = async ({ commit, dispatch, state, getters }) => {
+ try {
+ /**
+ * Currently, we delete all existing links and then
+ * recreate new ones on each edit. This is because the
+ * backend doesn't support bulk updating of Release links,
+ * and updating individual links can lead to validation
+ * race conditions (in particular, the "URLs must be unique")
+ * constraint.
+ *
+ * This isn't ideal since this is no longer an atomic
+ * operation - parts of it can fail while others succeed,
+ * leaving the Release in an inconsistent state.
+ *
+ * This logic should be refactored to take place entirely
+ * in the backend. This is being discussed in
+ * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50300
+ */
+ const updateReleaseResponse = await gqClient.mutate({
+ mutation: updateReleaseMutation,
+ variables: getters.releaseUpdateMutatationVariables,
+ });
+
+ checkForErrorsAsData(
+ updateReleaseResponse,
+ 'releaseUpdate',
+ `Something went wrong while updating release with projectPath "${state.projectPath}" and tagName "${state.tagName}"`,
+ );
+
+ // Delete all links currently associated with this Release
+ await Promise.all(
+ getters.releaseLinksToDelete.map(({ id }) => deleteReleaseLinks({ state, id })),
+ );
+
+ // Create a new link for each link in the form
+ await Promise.all(
+ getters.releaseLinksToCreate.map((link) => createReleaseLink({ state, link })),
+ );
+
+ dispatch('receiveSaveReleaseSuccess', state.release._links.self);
+ } catch (error) {
+ commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while saving the release details.'));
+ }
+};
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index 831037c8861..d83ec05872a 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -103,3 +103,39 @@ export const isValid = (_state, getters) => {
const errors = getters.validationErrors;
return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty;
};
+
+/** Returns all the variables for a `releaseUpdate` GraphQL mutation */
+export const releaseUpdateMutatationVariables = (state) => {
+ const name = state.release.name?.trim().length > 0 ? state.release.name.trim() : null;
+
+ // Milestones may be either a list of milestone objects OR just a list
+ // of milestone titles. The GraphQL mutation requires only the titles be sent.
+ const milestones = (state.release.milestones || []).map((m) => m.title || m);
+
+ return {
+ input: {
+ projectPath: state.projectPath,
+ tagName: state.release.tagName,
+ name,
+ description: state.release.description,
+ milestones,
+ },
+ };
+};
+
+/** Returns all the variables for a `releaseCreate` GraphQL mutation */
+export const releaseCreateMutatationVariables = (state, getters) => {
+ return {
+ input: {
+ ...getters.releaseUpdateMutatationVariables.input,
+ ref: state.createFrom,
+ assets: {
+ links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
+ name,
+ url,
+ linkType: linkType.toUpperCase(),
+ })),
+ },
+ },
+ };
+};
diff --git a/app/assets/javascripts/releases/stores/modules/index/actions.js b/app/assets/javascripts/releases/stores/modules/index/actions.js
index f1add54626a..00be25f089b 100644
--- a/app/assets/javascripts/releases/stores/modules/index/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/index/actions.js
@@ -1,45 +1,21 @@
-import api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import {
- normalizeHeaders,
- parseIntPagination,
- convertObjectPropsToCamelCase,
-} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
-import { PAGE_SIZE } from '../../../constants';
-import { gqClient, convertAllReleasesGraphQLResponse } from '../../../util';
+import { PAGE_SIZE } from '~/releases/constants';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+import { gqClient, convertAllReleasesGraphQLResponse } from '~/releases/util';
import * as types from './mutation_types';
/**
- * Gets a paginated list of releases from the server
+ * Gets a paginated list of releases from the GraphQL endpoint
*
* @param {Object} vuexParams
* @param {Object} actionParams
- * @param {Number} [actionParams.page] The page number of results to fetch
- * (this parameter is only used when fetching results from the REST API)
* @param {String} [actionParams.before] A GraphQL cursor. If provided,
- * the items returned will proceed the provided cursor (this parameter is only
- * used when fetching results from the GraphQL API).
+ * the items returned will proceed the provided cursor.
* @param {String} [actionParams.after] A GraphQL cursor. If provided,
- * the items returned will follow the provided cursor (this parameter is only
- * used when fetching results from the GraphQL API).
+ * the items returned will follow the provided cursor.
*/
-export const fetchReleases = ({ dispatch, rootGetters }, { page = 1, before, after }) => {
- if (rootGetters.useGraphQLEndpoint) {
- dispatch('fetchReleasesGraphQl', { before, after });
- } else {
- dispatch('fetchReleasesRest', { page });
- }
-};
-
-/**
- * Gets a paginated list of releases from the GraphQL endpoint
- */
-export const fetchReleasesGraphQl = (
- { dispatch, commit, state },
- { before = null, after = null },
-) => {
+export const fetchReleases = ({ dispatch, commit, state }, { before, after }) => {
commit(types.REQUEST_RELEASES);
const { sort, orderBy } = state.sorting;
@@ -55,7 +31,7 @@ export const fetchReleasesGraphQl = (
paginationParams = { first: PAGE_SIZE, after };
} else {
throw new Error(
- 'Both a `before` and an `after` parameter were provided to fetchReleasesGraphQl. These parameters cannot be used together.',
+ 'Both a `before` and an `after` parameter were provided to fetchReleases. These parameters cannot be used together.',
);
}
@@ -69,33 +45,11 @@ export const fetchReleasesGraphQl = (
},
})
.then((response) => {
- const { data, paginationInfo: graphQlPageInfo } = convertAllReleasesGraphQLResponse(response);
+ const { data, paginationInfo: pageInfo } = convertAllReleasesGraphQLResponse(response);
commit(types.RECEIVE_RELEASES_SUCCESS, {
data,
- graphQlPageInfo,
- });
- })
- .catch(() => dispatch('receiveReleasesError'));
-};
-
-/**
- * Gets a paginated list of releases from the REST endpoint
- */
-export const fetchReleasesRest = ({ dispatch, commit, state }, { page }) => {
- commit(types.REQUEST_RELEASES);
-
- const { sort, orderBy } = state.sorting;
-
- api
- .releases(state.projectId, { page, sort, order_by: orderBy })
- .then(({ data, headers }) => {
- const restPageInfo = parseIntPagination(normalizeHeaders(headers));
- const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true });
-
- commit(types.RECEIVE_RELEASES_SUCCESS, {
- data: camelCasedReleases,
- restPageInfo,
+ pageInfo,
});
})
.catch(() => dispatch('receiveReleasesError'));
diff --git a/app/assets/javascripts/releases/stores/modules/index/mutations.js b/app/assets/javascripts/releases/stores/modules/index/mutations.js
index e1aaa2e2a19..55a8a488be8 100644
--- a/app/assets/javascripts/releases/stores/modules/index/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/index/mutations.js
@@ -17,12 +17,11 @@ export default {
* @param {Object} state
* @param {Object} resp
*/
- [types.RECEIVE_RELEASES_SUCCESS](state, { data, restPageInfo, graphQlPageInfo }) {
+ [types.RECEIVE_RELEASES_SUCCESS](state, { data, pageInfo }) {
state.hasError = false;
state.isLoading = false;
state.releases = data;
- state.restPageInfo = restPageInfo;
- state.graphQlPageInfo = graphQlPageInfo;
+ state.pageInfo = pageInfo;
},
/**
@@ -36,8 +35,7 @@ export default {
state.isLoading = false;
state.releases = [];
state.hasError = true;
- state.restPageInfo = {};
- state.graphQlPageInfo = {};
+ state.pageInfo = {};
},
[types.SET_SORTING](state, sorting) {
diff --git a/app/assets/javascripts/releases/stores/modules/index/state.js b/app/assets/javascripts/releases/stores/modules/index/state.js
index 164a496d450..5e1aaab7b58 100644
--- a/app/assets/javascripts/releases/stores/modules/index/state.js
+++ b/app/assets/javascripts/releases/stores/modules/index/state.js
@@ -16,8 +16,7 @@ export default ({
isLoading: false,
hasError: false,
releases: [],
- restPageInfo: {},
- graphQlPageInfo: {},
+ pageInfo: {},
sorting: {
sort: DESCENDING_ORDER,
orderBy: RELEASED_AT,
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index 36c17b5b252..22d5fb4f620 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -1,50 +1,7 @@
import { pick } from 'lodash';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
-import {
- convertObjectPropsToCamelCase,
- convertObjectPropsToSnakeCase,
-} from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
-/**
- * Converts a release object into a JSON object that can sent to the public
- * API to create or update a release.
- * @param {Object} release The release object to convert
- * @param {string} createFrom The ref to create a new tag from, if necessary
- */
-export const releaseToApiJson = (release, createFrom = null) => {
- const name = release.name?.trim().length > 0 ? release.name.trim() : null;
-
- // Milestones may be either a list of milestone objects OR just a list
- // of milestone titles. The API requires only the titles be sent.
- const milestones = (release.milestones || []).map((m) => m.title || m);
-
- return convertObjectPropsToSnakeCase(
- {
- name,
- tagName: release.tagName,
- ref: createFrom,
- description: release.description,
- milestones,
- assets: release.assets,
- },
- { deep: true },
- );
-};
-
-/**
- * Converts a JSON release object returned by the Release API
- * into the structure this Vue application can work with.
- * @param {Object} json The JSON object received from the release API
- */
-export const apiJsonToRelease = (json) => {
- const release = convertObjectPropsToCamelCase(json, { deep: true });
-
- release.milestones = release.milestones || [];
-
- return release;
-};
-
export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE });
const convertScalarProperties = (graphQLRelease) =>
@@ -52,24 +9,37 @@ const convertScalarProperties = (graphQLRelease) =>
'name',
'tagName',
'tagPath',
+ 'description',
'descriptionHtml',
'releasedAt',
'upcomingRelease',
]);
-const convertAssets = (graphQLRelease) => ({
- assets: {
- count: graphQLRelease.assets.count,
- sources: [...graphQLRelease.assets.sources.nodes],
- links: graphQLRelease.assets.links.nodes.map((l) => ({
+const convertAssets = (graphQLRelease) => {
+ let sources = [];
+ if (graphQLRelease.assets.sources?.nodes) {
+ sources = [...graphQLRelease.assets.sources.nodes];
+ }
+
+ let links = [];
+ if (graphQLRelease.assets.links?.nodes) {
+ links = graphQLRelease.assets.links.nodes.map((l) => ({
...l,
linkType: l.linkType?.toLowerCase(),
- })),
- },
-});
+ }));
+ }
+
+ return {
+ assets: {
+ count: graphQLRelease.assets.count,
+ sources,
+ links,
+ },
+ };
+};
const convertEvidences = (graphQLRelease) => ({
- evidences: graphQLRelease.evidences.nodes.map((e) => e),
+ evidences: (graphQLRelease.evidences?.nodes ?? []).map((e) => ({ ...e })),
});
const convertLinks = (graphQLRelease) => ({
@@ -100,18 +70,19 @@ const convertMilestones = (graphQLRelease) => ({
...m,
webUrl: m.webPath,
webPath: undefined,
- issueStats: {
- total: m.stats.totalIssuesCount,
- closed: m.stats.closedIssuesCount,
- },
+ issueStats: m.stats
+ ? {
+ total: m.stats.totalIssuesCount,
+ closed: m.stats.closedIssuesCount,
+ }
+ : {},
stats: undefined,
})),
});
/**
* Converts a single release object fetched from GraphQL
- * into a release object that matches the shape of the REST API
- * (the same shape that is returned by `apiJsonToRelease` above.)
+ * into a release object that matches the general structure of the REST API
*
* @param graphQLRelease The release object returned from a GraphQL query
*/
diff --git a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
index e5980f1e539..dabfb623f43 100644
--- a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
+++ b/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
@@ -6,7 +6,7 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import ReportLink from '~/reports/components/report_link.vue';
-import { STATUS_SUCCESS } from '~/reports/constants';
+import { STATUS_SUCCESS, STATUS_NEUTRAL } from '~/reports/constants';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '../constants';
export default {
@@ -21,7 +21,8 @@ export default {
props: {
status: {
type: String,
- required: true,
+ required: false,
+ default: STATUS_NEUTRAL,
},
issue: {
type: Object,
diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
index d293165ef2f..3287ba691bf 100644
--- a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
+++ b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { s__, sprintf } from '~/locale';
import { componentNames } from '~/reports/components/issue_body';
import ReportSection from '~/reports/components/report_section.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createStore from './store';
export default {
@@ -12,26 +11,12 @@ export default {
components: {
ReportSection,
},
- mixins: [glFeatureFlagsMixin()],
props: {
- headPath: {
- type: String,
- required: true,
- },
- headBlobPath: {
- type: String,
- required: true,
- },
basePath: {
type: String,
required: false,
default: null,
},
- baseBlobPath: {
- type: String,
- required: false,
- default: null,
- },
codequalityReportsPath: {
type: String,
required: false,
@@ -55,9 +40,6 @@ export default {
created() {
this.setPaths({
basePath: this.basePath,
- headPath: this.headPath,
- baseBlobPath: this.baseBlobPath,
- headBlobPath: this.headBlobPath,
reportsPath: this.codequalityReportsPath,
helpPath: this.codequalityHelpPath,
});
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
index ddd1747899f..e3238207af2 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -1,34 +1,23 @@
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
-import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison';
+import { parseCodeclimateMetrics } from './utils/codequality_parser';
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-export const fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => {
+export const fetchReports = ({ state, dispatch, commit }) => {
commit(types.REQUEST_REPORTS);
- if (diffFeatureFlagEnabled) {
- return axios
- .get(state.reportsPath)
- .then(({ data }) => {
- return dispatch('receiveReportsSuccess', {
- newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
- resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
- });
- })
- .catch((error) => dispatch('receiveReportsError', error));
- }
if (!state.basePath) {
return dispatch('receiveReportsError');
}
- return Promise.all([axios.get(state.headPath), axios.get(state.basePath)])
- .then((results) =>
- doCodeClimateComparison(
- parseCodeclimateMetrics(results[0].data, state.headBlobPath),
- parseCodeclimateMetrics(results[1].data, state.baseBlobPath),
- ),
- )
- .then((data) => dispatch('receiveReportsSuccess', data))
+ return axios
+ .get(state.reportsPath)
+ .then(({ data }) => {
+ return dispatch('receiveReportsSuccess', {
+ newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
+ resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
+ });
+ })
.catch((error) => dispatch('receiveReportsError', error));
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
index 095e6637966..8edeb6cc976 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -3,9 +3,6 @@ import * as types from './mutation_types';
export default {
[types.SET_PATHS](state, paths) {
state.basePath = paths.basePath;
- state.headPath = paths.headPath;
- state.baseBlobPath = paths.baseBlobPath;
- state.headBlobPath = paths.headBlobPath;
state.reportsPath = paths.reportsPath;
state.helpPath = paths.helpPath;
},
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js
index b252c8c9817..a794f5f0577 100644
--- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
+++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js
@@ -1,5 +1,3 @@
-import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
-
export const parseCodeclimateMetrics = (issues = [], path = '') => {
return issues.map((issue) => {
const parsedIssue = {
@@ -27,17 +25,3 @@ export const parseCodeclimateMetrics = (issues = [], path = '') => {
return parsedIssue;
});
};
-
-export const doCodeClimateComparison = (headIssues, baseIssues) => {
- // Do these comparisons in worker threads to avoid blocking the main thread
- return new Promise((resolve, reject) => {
- const worker = new CodeQualityComparisonWorker();
- worker.addEventListener('message', ({ data }) =>
- data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
- );
- worker.postMessage({
- headIssues,
- baseIssues,
- });
- });
-};
diff --git a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
deleted file mode 100644
index ae389d266f8..00000000000
--- a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { differenceBy } from 'lodash';
-
-const KEY_TO_FILTER_BY = 'fingerprint';
-
-// eslint-disable-next-line no-restricted-globals
-self.addEventListener('message', (e) => {
- const { data } = e;
-
- if (data === undefined) {
- return null;
- }
-
- const { headIssues, baseIssues } = data;
-
- if (!headIssues || !baseIssues) {
- // eslint-disable-next-line no-restricted-globals
- return self.postMessage({});
- }
-
- // eslint-disable-next-line no-restricted-globals
- self.postMessage({
- newIssues: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY),
- resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY),
- });
-
- // eslint-disable-next-line no-restricted-globals
- return self.close();
-});
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 12b5cb9f207..7a490210f0b 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -190,10 +190,14 @@ export default {
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
<div class="media-body d-flex flex-align-self-center align-items-center">
<div data-testid="report-section-code-text" class="js-code-text code-text">
- <div>
- {{ headerText }}
+ <div class="gl-display-flex gl-align-items-center">
+ <p class="gl-line-height-normal gl-m-0">{{ headerText }}</p>
<slot :name="slotName"></slot>
- <popover v-if="hasPopover" :options="popoverOptions" class="gl-ml-2" />
+ <popover
+ v-if="hasPopover"
+ :options="popoverOptions"
+ class="gl-ml-2 gl-display-inline-flex"
+ />
</div>
<slot name="sub-heading"></slot>
</div>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 58b42fb7859..a9701c8f8aa 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -3,22 +3,21 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
+import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash';
import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql';
-import projectPathQuery from '../queries/project_path.query.graphql';
+import BlobHeaderEdit from './blob_header_edit.vue';
export default {
components: {
BlobHeader,
+ BlobHeaderEdit,
BlobContent,
GlLoadingIcon,
},
apollo: {
- projectPath: {
- query: projectPathQuery,
- },
- blobInfo: {
+ project: {
query: blobInfoQuery,
variables() {
return {
@@ -26,6 +25,11 @@ export default {
filePath: this.path,
};
},
+ result() {
+ this.switchViewer(
+ this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
+ );
+ },
error() {
createFlash({ message: __('An error occurred while loading the file. Please try again.') });
},
@@ -41,43 +45,70 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- projectPath: '',
- blobInfo: {
- name: '',
- size: '',
- rawBlob: '',
- type: '',
- fileType: '',
- tooLarge: false,
- path: '',
- editBlobPath: '',
- ideEditPath: '',
- storedExternally: false,
- rawPath: '',
- externalStorageUrl: '',
- replacePath: '',
- deletePath: '',
- canLock: false,
- isLocked: false,
- lockLink: '',
- canModifyBlob: true,
- forkPath: '',
- simpleViewer: '',
- richViewer: '',
+ activeViewerType: SIMPLE_BLOB_VIEWER,
+ project: {
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ name: '',
+ size: '',
+ rawTextBlob: '',
+ type: '',
+ fileType: '',
+ tooLarge: false,
+ path: '',
+ editBlobPath: '',
+ ideEditPath: '',
+ storedExternally: false,
+ rawPath: '',
+ externalStorageUrl: '',
+ replacePath: '',
+ deletePath: '',
+ canLock: false,
+ isLocked: false,
+ lockLink: '',
+ canModifyBlob: true,
+ forkPath: '',
+ simpleViewer: {},
+ richViewer: null,
+ },
+ ],
+ },
+ },
},
};
},
computed: {
isLoading() {
- return this.$apollo.queries.blobInfo.loading;
+ return this.$apollo.queries.project.loading;
},
- viewer() {
- const { fileType, tooLarge, type } = this.blobInfo;
+ blobInfo() {
+ const nodes = this.project?.repository?.blobs?.nodes;
- return { fileType, tooLarge, type };
+ return nodes[0] || {};
+ },
+ viewer() {
+ const { richViewer, simpleViewer } = this.blobInfo;
+ return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
+ },
+ hasRichViewer() {
+ return Boolean(this.blobInfo.richViewer);
+ },
+ hasRenderError() {
+ return Boolean(this.viewer.renderError);
+ },
+ },
+ methods: {
+ switchViewer(newViewer) {
+ this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
},
},
};
@@ -86,11 +117,21 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" />
- <div v-if="blobInfo && !isLoading">
- <blob-header :blob="blobInfo" />
+ <div v-if="blobInfo && !isLoading" class="file-holder">
+ <blob-header
+ :blob="blobInfo"
+ :hide-viewer-switcher="!hasRichViewer"
+ :active-viewer-type="viewer.type"
+ :has-render-error="hasRenderError"
+ @viewer-changed="switchViewer"
+ >
+ <template #actions>
+ <blob-header-edit :edit-path="blobInfo.editBlobPath" />
+ </template>
+ </blob-header>
<blob-content
:blob="blobInfo"
- :content="blobInfo.rawBlob"
+ :content="blobInfo.rawTextBlob"
:is-raw-content="true"
:active-viewer="viewer"
:loading="false"
diff --git a/app/assets/javascripts/repository/components/blob_header_edit.vue b/app/assets/javascripts/repository/components/blob_header_edit.vue
new file mode 100644
index 00000000000..f3649895736
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_header_edit.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ edit: __('Edit'),
+ },
+ components: {
+ GlButton,
+ },
+ props: {
+ editPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button category="primary" variant="confirm" class="gl-mr-3" :href="editPath">
+ {{ $options.i18n.edit }}
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index d2ff01e7fc1..aa087d4c631 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -93,7 +93,7 @@ export default {
text: PRIMARY_OPTIONS_TEXT,
attributes: [
{
- variant: 'success',
+ variant: 'confirm',
loading: this.loading,
disabled: !this.formCompleted || this.loading,
},
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 3a9a2adb417..501ae7e9f2f 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -4,6 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
+import PerformancePlugin from '~/performance/vue_performance_plugin';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -17,6 +18,10 @@ import createRouter from './router';
import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title';
+Vue.use(PerformancePlugin, {
+ components: ['SimpleViewer', 'BlobContent'],
+});
+
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
diff --git a/app/assets/javascripts/repository/pages/blob.vue b/app/assets/javascripts/repository/pages/blob.vue
index 27af398be09..2645b294096 100644
--- a/app/assets/javascripts/repository/pages/blob.vue
+++ b/app/assets/javascripts/repository/pages/blob.vue
@@ -13,10 +13,14 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
};
</script>
<template>
- <blob-content-viewer :path="path" />
+ <blob-content-viewer :path="path" :project-path="projectPath" />
</template>
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index e0bbf12f3eb..07c076af54b 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -2,28 +2,32 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
project(fullPath: $projectPath) {
id
repository {
- blobs(path: $filePath) {
- name
- size
- rawBlob
- type
- fileType
- tooLarge
- path
- editBlobPath
- ideEditPath
- storedExternally
- rawPath
- externalStorageUrl
- replacePath
- deletePath
- canLock
- isLocked
- lockLink
- canModifyBlob
- forkPath
- simpleViewer
- richViewer
+ blobs(paths: [$filePath]) {
+ nodes {
+ webPath
+ name
+ size
+ rawSize
+ rawTextBlob
+ fileType
+ path
+ editBlobPath
+ storedExternally
+ rawPath
+ replacePath
+ simpleViewer {
+ fileType
+ tooLarge
+ type
+ renderError
+ }
+ richViewer {
+ fileType
+ tooLarge
+ type
+ renderError
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index c7f7451fb55..6637d03a7a4 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -20,6 +20,7 @@ export default function createRouter(base, baseRef) {
component: BlobPage,
props: (route) => ({
path: route.params.path,
+ projectPath: base,
}),
};
diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue
new file mode 100644
index 00000000000..dd4fff3a77a
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_type_badge.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+
+const badge = {
+ [INSTANCE_TYPE]: {
+ variant: 'success',
+ text: s__('Runners|shared'),
+ },
+ [GROUP_TYPE]: {
+ variant: 'success',
+ text: s__('Runners|group'),
+ },
+ [PROJECT_TYPE]: {
+ variant: 'info',
+ text: s__('Runners|specific'),
+ },
+};
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ variant() {
+ return badge[this.type]?.variant;
+ },
+ text() {
+ return badge[this.type]?.text;
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge v-if="text" :variant="variant" v-bind="$attrs">
+ {{ text }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
new file mode 100644
index 00000000000..de3a3fda47e
--- /dev/null
+++ b/app/assets/javascripts/runner/constants.js
@@ -0,0 +1,11 @@
+import { s__ } from '~/locale';
+
+export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
+
+export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
+
+// CiRunnerType
+
+export const INSTANCE_TYPE = 'INSTANCE_TYPE';
+export const GROUP_TYPE = 'GROUP_TYPE';
+export const PROJECT_TYPE = 'PROJECT_TYPE';
diff --git a/app/assets/javascripts/runner/graphql/get_runner.query.graphql b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
new file mode 100644
index 00000000000..d209313d4df
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
@@ -0,0 +1,6 @@
+query getRunner($id: CiRunnerID!) {
+ runner(id: $id) {
+ id
+ runnerType
+ }
+}
diff --git a/app/assets/javascripts/runner/runner_details/constants.js b/app/assets/javascripts/runner/runner_details/constants.js
deleted file mode 100644
index bb57e85fa8a..00000000000
--- a/app/assets/javascripts/runner/runner_details/constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { s__ } from '~/locale';
-
-export const I18N_TITLE = s__('Runners|Runner #%{runner_id}');
diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/runner_details/index.js
index cbf70640ef7..05e6f86869d 100644
--- a/app/assets/javascripts/runner/runner_details/index.js
+++ b/app/assets/javascripts/runner/runner_details/index.js
@@ -1,7 +1,11 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import RunnerDetailsApp from './runner_details_app.vue';
-export const initRunnerDetail = (selector = '#js-runner-detail') => {
+Vue.use(VueApollo);
+
+export const initRunnerDetail = (selector = '#js-runner-details') => {
const el = document.querySelector(selector);
if (!el) {
@@ -10,8 +14,18 @@ export const initRunnerDetail = (selector = '#js-runner-detail') => {
const { runnerId } = el.dataset;
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
+ });
+
return new Vue({
el,
+ apolloProvider,
render(h) {
return h(RunnerDetailsApp, {
props: {
diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
index 1b1485bfe72..4736e547cb9 100644
--- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue
+++ b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
@@ -1,9 +1,15 @@
<script>
-import { I18N_TITLE } from './constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import RunnerTypeBadge from '../components/runner_type_badge.vue';
+import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
+import getRunnerQuery from '../graphql/get_runner.query.graphql';
export default {
+ components: {
+ RunnerTypeBadge,
+ },
i18n: {
- I18N_TITLE,
+ I18N_DETAILS_TITLE,
},
props: {
runnerId: {
@@ -11,10 +17,27 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ runner: {},
+ };
+ },
+ apollo: {
+ runner: {
+ query: getRunnerQuery,
+ variables() {
+ return {
+ id: convertToGraphQLId(RUNNER_ENTITY_TYPE, this.runnerId),
+ };
+ },
+ },
+ },
};
</script>
<template>
<h2 class="page-title">
- {{ sprintf($options.i18n.I18N_TITLE, { runner_id: runnerId }) }}
+ {{ sprintf($options.i18n.I18N_DETAILS_TITLE, { runner_id: runnerId }) }}
+
+ <runner-type-badge v-if="runner.runnerType" :type="runner.runnerType" />
</h2>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
index 4a3f988296c..2110af1522b 100644
--- a/app/assets/javascripts/security_configuration/components/configuration_table.vue
+++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue
@@ -1,6 +1,7 @@
<script>
import { GlLink, GlTable, GlAlert } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
+import ManageViaMR from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
@@ -11,8 +12,8 @@ import {
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
-import ManageSast from './manage_sast.vue';
-import { scanners } from './scanners_constants';
+
+import { scanners } from './constants';
import Upgrade from './upgrade.vue';
const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
@@ -40,7 +41,7 @@ export default {
},
getComponentForItem(item) {
const COMPONENTS = {
- [REPORT_TYPE_SAST]: ManageSast,
+ [REPORT_TYPE_SAST]: ManageViaMR,
[REPORT_TYPE_DAST]: Upgrade,
[REPORT_TYPE_DAST_PROFILES]: Upgrade,
[REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
@@ -49,7 +50,6 @@ export default {
[REPORT_TYPE_API_FUZZING]: Upgrade,
[REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
};
-
return COMPONENTS[item.type];
},
},
@@ -95,7 +95,12 @@ export default {
</template>
<template #cell(manage)="{ item }">
- <component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
+ <component
+ :is="getComponentForItem(item)"
+ :feature="item"
+ :data-testid="item.type"
+ @error="onError"
+ />
</template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/security_configuration/components/scanners_constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 9846df0b4bf..3cdcac4c0b4 100644
--- a/app/assets/javascripts/security_configuration/components/scanners_constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -1,6 +1,7 @@
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
@@ -134,3 +135,18 @@ export const scanners = [
type: REPORT_TYPE_LICENSE_COMPLIANCE,
},
];
+
+export const featureToMutationMap = {
+ [REPORT_TYPE_SAST]: {
+ mutationId: 'configureSast',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastMutation,
+ variables: {
+ input: {
+ projectPath,
+ configuration: { global: [], pipeline: [], analyzers: [] },
+ },
+ },
+ }),
+ },
+};
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
new file mode 100644
index 00000000000..518a6ede3de
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -0,0 +1,150 @@
+<script>
+import { GlButton, GlCard, GlIcon, GlLink } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlCard,
+ GlIcon,
+ GlLink,
+ ManageViaMr,
+ },
+ props: {
+ feature: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ available() {
+ return this.feature.available;
+ },
+ enabled() {
+ return this.available && this.feature.configured;
+ },
+ hasStatus() {
+ return !this.available || typeof this.feature.configured === 'boolean';
+ },
+ shortName() {
+ return this.feature.shortName ?? this.feature.name;
+ },
+ configurationButton() {
+ const button = this.enabled
+ ? {
+ text: this.$options.i18n.configureFeature,
+ category: 'secondary',
+ }
+ : {
+ text: this.$options.i18n.enableFeature,
+ category: 'primary',
+ };
+
+ button.text = sprintf(button.text, { feature: this.shortName });
+
+ return button;
+ },
+ showManageViaMr() {
+ const { available, configured, canEnableByMergeRequest } = this.feature;
+ return canEnableByMergeRequest && available && !configured;
+ },
+ cardClasses() {
+ return { 'gl-bg-gray-10': !this.available };
+ },
+ statusClasses() {
+ const { enabled } = this;
+
+ return {
+ 'gl-ml-auto': true,
+ 'gl-flex-shrink-0': true,
+ 'gl-text-gray-500': !enabled,
+ 'gl-text-green-500': enabled,
+ };
+ },
+ hasSecondary() {
+ const { name, description, configurationText } = this.feature.secondary ?? {};
+ return Boolean(name && description && configurationText);
+ },
+ },
+ i18n: {
+ enabled: s__('SecurityConfiguration|Enabled'),
+ notEnabled: s__('SecurityConfiguration|Not enabled'),
+ availableWith: s__('SecurityConfiguration|Available with Ultimate'),
+ configurationGuide: s__('SecurityConfiguration|Configuration guide'),
+ configureFeature: s__('SecurityConfiguration|Configure %{feature}'),
+ enableFeature: s__('SecurityConfiguration|Enable %{feature}'),
+ learnMore: __('Learn more'),
+ },
+};
+</script>
+
+<template>
+ <gl-card :class="cardClasses">
+ <div class="gl-display-flex gl-align-items-baseline">
+ <h3 class="gl-font-lg gl-m-0 gl-mr-3">{{ feature.name }}</h3>
+
+ <div :class="statusClasses" data-testid="feature-status">
+ <template v-if="hasStatus">
+ <template v-if="enabled">
+ <gl-icon name="check-circle-filled" />
+ <span class="gl-text-green-700">{{ $options.i18n.enabled }}</span>
+ </template>
+
+ <template v-else-if="available">
+ {{ $options.i18n.notEnabled }}
+ </template>
+
+ <template v-else>
+ {{ $options.i18n.availableWith }}
+ </template>
+ </template>
+ </div>
+ </div>
+
+ <p class="gl-mb-0 gl-mt-5">
+ {{ feature.description }}
+ <gl-link :href="feature.helpPath">{{ $options.i18n.learnMore }}</gl-link>
+ </p>
+
+ <template v-if="available">
+ <gl-button
+ v-if="feature.configurationPath"
+ :href="feature.configurationPath"
+ variant="confirm"
+ :category="configurationButton.category"
+ class="gl-mt-5"
+ >
+ {{ configurationButton.text }}
+ </gl-button>
+
+ <manage-via-mr
+ v-else-if="showManageViaMr"
+ :feature="feature"
+ variant="confirm"
+ category="primary"
+ class="gl-mt-5"
+ />
+
+ <gl-button v-else icon="external-link" :href="feature.configurationHelpPath" class="gl-mt-5">
+ {{ $options.i18n.configurationGuide }}
+ </gl-button>
+ </template>
+
+ <div v-if="hasSecondary" data-testid="secondary-feature">
+ <h4 class="gl-font-base gl-m-0 gl-mt-6">{{ feature.secondary.name }}</h4>
+
+ <p class="gl-mb-0 gl-mt-5">{{ feature.secondary.description }}</p>
+
+ <gl-button
+ v-if="available && feature.secondary.configurationPath"
+ :href="feature.secondary.configurationPath"
+ variant="confirm"
+ category="secondary"
+ class="gl-mt-5"
+ >
+ {{ feature.secondary.configurationText }}
+ </gl-button>
+ </div>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/manage_sast.vue b/app/assets/javascripts/security_configuration/components/manage_sast.vue
deleted file mode 100644
index 8a8827b41cd..00000000000
--- a/app/assets/javascripts/security_configuration/components/manage_sast.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { redirectTo } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
-import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
-
-export default {
- components: {
- GlButton,
- },
- inject: {
- projectPath: {
- from: 'projectPath',
- default: '',
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- methods: {
- async mutate() {
- this.isLoading = true;
- try {
- const { data } = await this.$apollo.mutate({
- mutation: configureSastMutation,
- variables: {
- input: {
- projectPath: this.projectPath,
- configuration: { global: [], pipeline: [], analyzers: [] },
- },
- },
- });
- const { errors, successPath } = data.configureSast;
-
- if (errors.length > 0) {
- throw new Error(errors[0]);
- }
-
- if (!successPath) {
- throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
- }
-
- redirectTo(successPath);
- } catch (e) {
- this.$emit('error', e.message);
- this.isLoading = false;
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
- s__('SecurityConfiguration|Configure via merge request')
- }}</gl-button>
-</template>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue
index 518eb57731d..2541c29224a 100644
--- a/app/assets/javascripts/security_configuration/components/upgrade.vue
+++ b/app/assets/javascripts/security_configuration/components/upgrade.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { UPGRADE_CTA } from './scanners_constants';
+import { UPGRADE_CTA } from './constants';
export default {
components: {
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index bff90254c04..c754af5c7de 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -67,11 +67,6 @@ export default {
required: false,
default: '',
},
- canSetUserAvailability: {
- type: Boolean,
- required: false,
- default: false,
- },
currentClearStatusAfter: {
type: String,
required: false,
@@ -292,7 +287,7 @@ export default {
</button>
</span>
</div>
- <div v-if="canSetUserAvailability" class="form-group">
+ <div class="form-group">
<div class="gl-display-flex">
<gl-form-checkbox
v-model="availability"
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 467cd321fb8..3ca9288b156 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
-import DueDateSelectors from '../../due_date_select';
+import initDatePicker from '~/behaviors/date_picker';
import GLForm from '../../gl_form';
import ZenMode from '../../zen_mode';
export default (initGFM = true) => {
new ZenMode(); // eslint-disable-line no-new
- new DueDateSelectors(); // eslint-disable-line no-new
+ initDatePicker();
+
// eslint-disable-next-line no-new
new GLForm($('.milestone-form'), {
emojis: true,
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index f98798582c1..e7ef731eed8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,6 +1,7 @@
<script>
-import actionCable from '~/actioncable_consumer';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import produce from 'immer';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType } from '~/issue_show/constants';
import { assigneesQueries } from '~/sidebar/constants';
export default {
@@ -12,60 +13,62 @@ export default {
required: false,
default: null,
},
- issuableIid: {
+ issuableType: {
type: String,
required: true,
},
- projectPath: {
- type: String,
+ issuableId: {
+ type: Number,
required: true,
},
- issuableType: {
- type: String,
+ queryVariables: {
+ type: Object,
required: true,
},
},
+ computed: {
+ issuableClass() {
+ return Object.keys(IssuableType).find((key) => IssuableType[key] === this.issuableType);
+ },
+ },
apollo: {
- workspace: {
+ issuable: {
query() {
return assigneesQueries[this.issuableType].query;
},
variables() {
- return {
- iid: this.issuableIid,
- fullPath: this.projectPath,
- };
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.workspace?.issuable;
},
- result(data) {
- if (this.mediator) {
- this.handleFetchResult(data);
- }
+ subscribeToMore: {
+ document() {
+ return assigneesQueries[this.issuableType].subscription;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId(this.issuableClass, this.issuableId),
+ };
+ },
+ updateQuery(prev, { subscriptionData }) {
+ if (prev && subscriptionData?.data?.issuableAssigneesUpdated) {
+ const data = produce(prev, (draftData) => {
+ draftData.workspace.issuable.assignees.nodes =
+ subscriptionData.data.issuableAssigneesUpdated.assignees.nodes;
+ });
+ if (this.mediator) {
+ this.handleFetchResult(data);
+ }
+ return data;
+ }
+ return prev;
+ },
},
},
},
- mounted() {
- this.initActionCablePolling();
- },
- beforeDestroy() {
- this.$options.subscription.unsubscribe();
- },
methods: {
- received(data) {
- if (data.event === 'updated') {
- this.$apollo.queries.workspace.refetch();
- }
- },
- initActionCablePolling() {
- this.$options.subscription = actionCable.subscriptions.create(
- {
- channel: 'IssuesChannel',
- project_path: this.projectPath,
- iid: this.issuableIid,
- },
- { received: this.received },
- );
- },
- handleFetchResult({ data }) {
+ handleFetchResult(data) {
const { nodes } = data.workspace.issuable.assignees;
const assignees = nodes.map((n) => ({
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index e93aced12f3..80caebad39d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -60,7 +60,7 @@ export default {
v-else
:users="users"
:issuable-type="issuableType"
- class="gl-mt-2 hide-collapsed"
+ class="gl-text-gray-800 gl-mt-2 hide-collapsed"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index e15ea595190..ca95599742a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -44,6 +44,10 @@ export default {
type: String,
required: true,
},
+ issuableId: {
+ type: Number,
+ required: true,
+ },
assigneeAvailabilityStatus: {
type: Object,
required: false,
@@ -61,6 +65,12 @@ export default {
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
},
+ queryVariables() {
+ return {
+ iid: this.issuableIid,
+ fullPath: this.projectPath,
+ };
+ },
relativeUrlRoot() {
return gon.relative_url_root ?? '';
},
@@ -121,9 +131,9 @@ export default {
<div>
<assignees-realtime
v-if="shouldEnableRealtime"
- :issuable-iid="issuableIid"
- :project-path="projectPath"
:issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :query-variables="queryVariables"
:mediator="mediator"
/>
<assignee-title
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 78cac989850..932be7addc0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,19 +1,17 @@
<script>
-import { GlDropdownItem, GlDropdownDivider, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
-import searchUsers from '~/graphql_shared/queries/users_search.query.graphql';
import { IssuableType } from '~/issue_show/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants';
-import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+import { assigneesQueries } from '~/sidebar/constants';
+import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SidebarInviteMembers from './sidebar_invite_members.vue';
-import SidebarParticipant from './sidebar_participant.vue';
export const assigneesWidget = Vue.observable({
updateAssignees: null,
@@ -33,23 +31,16 @@ export default {
components: {
SidebarEditableItem,
IssuableAssignees,
- MultiSelectDropdown,
GlDropdownItem,
- GlDropdownDivider,
- GlSearchBoxByType,
- GlLoadingIcon,
SidebarInviteMembers,
- SidebarParticipant,
SidebarAssigneesRealtime,
+ UserSelect,
},
mixins: [glFeatureFlagsMixin()],
inject: {
directlyInviteMembers: {
default: false,
},
- indirectlyInviteMembers: {
- default: false,
- },
},
props: {
iid: {
@@ -73,20 +64,21 @@ export default {
return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
},
},
- multipleAssignees: {
- type: Boolean,
+ issuableId: {
+ type: Number,
required: false,
- default: true,
+ default: null,
+ },
+ allowMultipleAssignees: {
+ type: Boolean,
+ required: true,
},
},
data() {
return {
- search: '',
issuable: {},
- searchUsers: [],
selected: [],
isSettingAssignees: false,
- isSearching: false,
isDirty: false,
};
},
@@ -104,51 +96,13 @@ export default {
result({ data }) {
const issuable = data.workspace?.issuable;
if (issuable) {
- this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes));
+ this.selected = cloneDeep(issuable.assignees.nodes);
}
},
error() {
createFlash({ message: __('An error occurred while fetching participants.') });
},
},
- searchUsers: {
- query: searchUsers,
- variables() {
- return {
- fullPath: this.fullPath,
- search: this.search,
- };
- },
- update(data) {
- const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || [];
- const filteredParticipants = this.participants.filter(
- (user) =>
- user.name.toLowerCase().includes(this.search.toLowerCase()) ||
- user.username.toLowerCase().includes(this.search.toLowerCase()),
- );
- const mergedSearchResults = searchResults.reduce((acc, current) => {
- // Some users are duplicated in the query result:
- // https://gitlab.com/gitlab-org/gitlab/-/issues/327822
- if (!acc.some((user) => current.username === user.username)) {
- acc.push(current);
- }
- return acc;
- }, filteredParticipants);
-
- return mergedSearchResults;
- },
- debounce: ASSIGNEES_DEBOUNCE_DELAY,
- skip() {
- return this.isSearchEmpty;
- },
- error() {
- createFlash({ message: __('An error occurred while searching users.') });
- this.isSearching = false;
- },
- result() {
- this.isSearching = false;
- },
- },
},
computed: {
shouldEnableRealtime() {
@@ -167,13 +121,6 @@ export default {
: this.issuable?.assignees?.nodes;
return currentAssignees || [];
},
- participants() {
- const users =
- this.isSearchEmpty || this.isSearching
- ? this.issuable?.participants?.nodes
- : this.searchUsers;
- return this.moveCurrentUserToStart(users);
- },
assigneeText() {
const items = this.$apollo.queries.issuable.loading ? this.initialAssignees : this.selected;
if (!items) {
@@ -181,28 +128,8 @@ export default {
}
return n__('Assignee', '%d Assignees', items.length);
},
- selectedFiltered() {
- if (this.isSearchEmpty || this.isSearching) {
- return this.selected;
- }
-
- const foundUsernames = this.searchUsers.map(({ username }) => username);
- return this.selected.filter(({ username }) => foundUsernames.includes(username));
- },
- unselectedFiltered() {
- return (
- this.participants?.filter(({ username }) => !this.selectedUserNames.includes(username)) ||
- []
- );
- },
- selectedIsEmpty() {
- return this.selectedFiltered.length === 0;
- },
- selectedUserNames() {
- return this.selected.map(({ username }) => username);
- },
- isSearchEmpty() {
- return this.search === '';
+ isAssigneesLoading() {
+ return !this.initialAssignees && this.$apollo.queries.issuable.loading;
},
currentUser() {
return {
@@ -211,35 +138,9 @@ export default {
avatarUrl: gon?.current_user_avatar_url,
};
},
- isAssigneesLoading() {
- return !this.initialAssignees && this.$apollo.queries.issuable.loading;
- },
- isCurrentUserInParticipants() {
- const isCurrentUser = (user) => user.username === this.currentUser.username;
- return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser);
- },
- noUsersFound() {
- return !this.isSearchEmpty && this.searchUsers.length === 0;
- },
signedIn() {
return this.currentUser.username !== undefined;
},
- showCurrentUser() {
- return (
- this.signedIn &&
- !this.isCurrentUserInParticipants &&
- (this.isSearchEmpty || this.isSearching)
- );
- },
- },
- watch: {
- // We need to add this watcher to track the moment when user is alredy typing
- // but query is still not started due to debounce
- search(newVal) {
- if (newVal) {
- this.isSearching = true;
- }
- },
},
created() {
assigneesWidget.updateAssignees = this.updateAssignees;
@@ -269,59 +170,15 @@ export default {
this.isSettingAssignees = false;
});
},
- selectAssignee(name) {
- this.isDirty = true;
-
- if (!this.multipleAssignees) {
- this.selected = name ? [name] : [];
- this.collapseWidget();
- return;
- }
- if (name === undefined) {
- this.clearSelected();
- return;
- }
- this.selected = this.selected.concat(name);
- },
- unselect(name) {
- this.selected = this.selected.filter((user) => user.username !== name);
- this.isDirty = true;
-
- if (!this.multipleAssignees) {
- this.collapseWidget();
- }
- },
assignSelf() {
- this.updateAssignees(this.currentUser.username);
- },
- clearSelected() {
- this.selected = [];
+ this.updateAssignees([this.currentUser.username]);
},
saveAssignees() {
- this.isDirty = false;
- this.updateAssignees(this.selectedUserNames);
- this.$el.dispatchEvent(hideDropdownEvent);
- },
- isChecked(id) {
- return this.selectedUserNames.includes(id);
- },
- async focusSearch() {
- await this.$nextTick();
- this.$refs.search.focusInput();
- },
- moveCurrentUserToStart(users) {
- if (!users) {
- return [];
- }
- const usersCopy = [...users];
- const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
-
- if (currentUser) {
- const index = usersCopy.indexOf(currentUser);
- usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
+ if (this.isDirty) {
+ this.isDirty = false;
+ this.updateAssignees(this.selected.map(({ username }) => username));
}
-
- return usersCopy;
+ this.$el.dispatchEvent(hideDropdownEvent);
},
collapseWidget() {
this.$refs.toggle.collapse();
@@ -329,8 +186,17 @@ export default {
expandWidget() {
this.$refs.toggle.expand();
},
- showDivider(list) {
- return list.length > 0 && this.isSearchEmpty;
+ focusSearch() {
+ this.$refs.userSelect.focusSearch();
+ },
+ showError() {
+ createFlash({ message: __('An error occurred while fetching participants.') });
+ },
+ setDirtyState() {
+ this.isDirty = true;
+ if (!this.allowMultipleAssignees) {
+ this.collapseWidget();
+ }
},
},
};
@@ -340,9 +206,9 @@ export default {
<div data-testid="assignees-widget">
<sidebar-assignees-realtime
v-if="shouldEnableRealtime"
- :project-path="fullPath"
- :issuable-iid="iid"
:issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :query-variables="queryVariables"
/>
<sidebar-editable-item
ref="toggle"
@@ -363,86 +229,27 @@ export default {
@expand-widget="expandWidget"
/>
</template>
-
<template #default>
- <multi-select-dropdown
- class="gl-w-full dropdown-menu-user"
+ <user-select
+ ref="userSelect"
+ v-model="selected"
:text="$options.i18n.assignees"
:header-text="$options.i18n.assignTo"
+ :iid="iid"
+ :full-path="fullPath"
+ :allow-multiple-assignees="allowMultipleAssignees"
+ :current-user="currentUser"
+ :issuable-type="issuableType"
+ class="gl-w-full dropdown-menu-user"
@toggle="collapseWidget"
+ @error="showError"
+ @input="setDirtyState"
>
- <template #search>
- <gl-search-box-by-type
- ref="search"
- v-model.trim="search"
- class="js-dropdown-input-field"
- />
- </template>
- <template #items>
- <gl-loading-icon
- v-if="$apollo.queries.searchUsers.loading || $apollo.queries.issuable.loading"
- data-testid="loading-participants"
- size="lg"
- />
- <template v-else>
- <template v-if="isSearchEmpty || isSearching">
- <gl-dropdown-item
- :is-checked="selectedIsEmpty"
- :is-check-centered="true"
- data-testid="unassign"
- @click="selectAssignee()"
- >
- <span
- :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
- class="gl-font-weight-bold"
- >{{ $options.i18n.unassigned }}</span
- ></gl-dropdown-item
- >
- </template>
- <gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
- <gl-dropdown-item
- v-for="item in selectedFiltered"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- :is-check-centered="true"
- data-testid="selected-participant"
- @click.stop="unselect(item.username)"
- >
- <sidebar-participant :user="item" />
- </gl-dropdown-item>
- <template v-if="showCurrentUser">
- <gl-dropdown-divider />
- <gl-dropdown-item
- data-testid="current-user"
- @click.stop="selectAssignee(currentUser)"
- >
- <sidebar-participant :user="currentUser" class="gl-pl-6!" />
- </gl-dropdown-item>
- </template>
- <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
- <gl-dropdown-item
- v-for="unselectedUser in unselectedFiltered"
- :key="unselectedUser.id"
- data-testid="unselected-participant"
- @click="selectAssignee(unselectedUser)"
- >
- <sidebar-participant :user="unselectedUser" class="gl-pl-6!" />
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="noUsersFound && !isSearching"
- data-testid="empty-results"
- class="gl-pl-6!"
- >
- {{ __('No matching results') }}
- </gl-dropdown-item>
- </template>
- </template>
<template #footer>
- <gl-dropdown-item>
- <sidebar-invite-members v-if="directlyInviteMembers || indirectlyInviteMembers" />
- </gl-dropdown-item>
- </template>
- </multi-select-dropdown>
+ <gl-dropdown-item v-if="directlyInviteMembers">
+ <sidebar-invite-members />
+ </gl-dropdown-item> </template
+ ></user-select>
</template>
</sidebar-editable-item>
</div>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
index 9952c6db582..5c32d03e0d4 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -1,51 +1,23 @@
<script>
-import InviteMemberModal from '~/invite_member/components/invite_member_modal.vue';
-import InviteMemberTrigger from '~/invite_member/components/invite_member_trigger.vue';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
export default {
displayText: __('Invite members'),
dataTrackLabel: 'edit_assignee',
+ dataTrackEvent: 'click_invite_members',
components: {
- InviteMemberTrigger,
- InviteMemberModal,
InviteMembersTrigger,
},
- inject: {
- projectMembersPath: {
- default: '',
- },
- directlyInviteMembers: {
- default: false,
- },
- },
- computed: {
- trackEvent() {
- return this.directlyInviteMembers ? 'click_invite_members' : 'click_invite_members_version_b';
- },
- },
};
</script>
<template>
- <div>
- <invite-members-trigger
- v-if="directlyInviteMembers"
- trigger-element="anchor"
- :display-text="$options.displayText"
- :event="trackEvent"
- :label="$options.dataTrackLabel"
- classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
- />
- <template v-else>
- <invite-member-trigger
- :display-text="$options.displayText"
- :event="trackEvent"
- :label="$options.dataTrackLabel"
- class="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
- />
- <invite-member-modal :members-path="projectMembersPath" />
- </template>
- </div>
+ <invite-members-trigger
+ trigger-element="anchor"
+ :display-text="$options.displayText"
+ :event="$options.dataTrackEvent"
+ :label="$options.dataTrackLabel"
+ classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
+ />
</template>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
new file mode 100644
index 00000000000..6a68e914b84
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -0,0 +1,296 @@
+<script>
+import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
+import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { dateFields, dateTypes, dueDateQueries, startDateQueries } from '~/sidebar/constants';
+import SidebarFormattedDate from './sidebar_formatted_date.vue';
+import SidebarInheritDate from './sidebar_inherit_date.vue';
+
+const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
+ bubbles: true,
+});
+
+export default {
+ tracking: {
+ event: 'click_edit_button',
+ label: 'right_sidebar',
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ GlDatepicker,
+ GlLink,
+ GlPopover,
+ SidebarEditableItem,
+ SidebarFormattedDate,
+ SidebarInheritDate,
+ },
+ inject: ['canUpdate'],
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ dateType: {
+ type: String,
+ required: false,
+ default: dateTypes.due,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ canInherit: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ issuable: {},
+ loading: false,
+ tracking: {
+ ...this.$options.tracking,
+ property: this.dateType === dateTypes.start ? 'startDate' : 'dueDate',
+ },
+ };
+ },
+ apollo: {
+ issuable: {
+ query() {
+ return this.dateQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.iid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable || {};
+ },
+ result({ data }) {
+ this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]);
+ },
+ error() {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} %{dateType} date.'),
+ {
+ issuableType: this.issuableType,
+ dateType: this.dateType === dateTypes.start ? 'start' : 'due',
+ },
+ ),
+ });
+ },
+ },
+ },
+ computed: {
+ dateQueries() {
+ return this.dateType === dateTypes.start ? startDateQueries : dueDateQueries;
+ },
+ dateLabel() {
+ return this.dateType === dateTypes.start
+ ? this.$options.i18n.startDate
+ : this.$options.i18n.dueDate;
+ },
+ removeDateLabel() {
+ return this.dateType === dateTypes.start
+ ? this.$options.i18n.removeStartDate
+ : this.$options.i18n.removeDueDate;
+ },
+ dateValue() {
+ return this.issuable?.[this.dateType] || null;
+ },
+ isLoading() {
+ return this.$apollo.queries.issuable.loading || this.loading;
+ },
+ hasDate() {
+ return this.dateValue !== null;
+ },
+ parsedDate() {
+ if (!this.hasDate) {
+ return null;
+ }
+
+ return parsePikadayDate(this.dateValue);
+ },
+ formattedDate() {
+ if (!this.hasDate) {
+ return this.$options.i18n.noDate;
+ }
+
+ return dateInWords(this.parsedDate, true);
+ },
+ workspacePath() {
+ return this.issuableType === IssuableType.Issue
+ ? {
+ projectPath: this.fullPath,
+ }
+ : {
+ groupPath: this.fullPath,
+ };
+ },
+ dataTestId() {
+ return this.dateType === dateTypes.start ? 'start-date' : 'due-date';
+ },
+ },
+ methods: {
+ closeForm() {
+ this.$refs.editable.collapse();
+ this.$el.dispatchEvent(hideDropdownEvent);
+ this.$emit('closeForm');
+ },
+ openDatePicker() {
+ this.$refs.datePicker.calendar.show();
+ },
+ setFixedDate(isFixed) {
+ const date = this.issuable[dateFields[this.dateType].dateFixed];
+ this.setDate(date, isFixed);
+ },
+ setDate(date, isFixed = true) {
+ const formattedDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
+ this.loading = true;
+ this.$refs.editable.collapse();
+ this.$apollo
+ .mutate({
+ mutation: this.dateQueries[this.issuableType].mutation,
+ variables: {
+ input: {
+ ...this.workspacePath,
+ iid: this.iid,
+ ...(this.canInherit
+ ? {
+ [dateFields[this.dateType].dateFixed]: isFixed ? formattedDate : undefined,
+ [dateFields[this.dateType].isDateFixed]: isFixed,
+ }
+ : {
+ [this.dateType]: formattedDate,
+ }),
+ },
+ },
+ })
+ .then(
+ ({
+ data: {
+ issuableSetDate: { errors },
+ },
+ }) => {
+ if (errors.length) {
+ createFlash({
+ message: errors[0],
+ });
+ } else {
+ this.$emit('closeForm');
+ }
+ },
+ )
+ .catch(() => {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} %{dateType} date.'),
+ {
+ issuableType: this.issuableType,
+ dateType: this.dateType === dateTypes.start ? 'start' : 'due',
+ },
+ ),
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ },
+ i18n: {
+ dueDate: __('Due date'),
+ startDate: __('Start date'),
+ noDate: __('None'),
+ removeDueDate: __('remove due date'),
+ removeStartDate: __('remove start date'),
+ dateHelpValidMessage: __(
+ 'These dates affect how your epics appear in the roadmap. Set a fixed date or one inherited from the milestones assigned to issues in this epic.',
+ ),
+ help: __('Help'),
+ learnMore: __('Learn more'),
+ },
+ dateHelpUrl: '/help/user/group/epics/index.md#start-date-and-due-date',
+};
+</script>
+
+<template>
+ <sidebar-editable-item
+ ref="editable"
+ :title="dateLabel"
+ :tracking="tracking"
+ :loading="isLoading"
+ class="block"
+ :data-testid="dataTestId"
+ @open="openDatePicker"
+ >
+ <template v-if="canInherit" #title-extra>
+ <gl-icon
+ ref="epicDatePopover"
+ name="question-o"
+ class="gl-ml-3 gl-cursor-pointer gl-text-blue-600 hide-collapsed"
+ tabindex="0"
+ :aria-label="$options.i18n.help"
+ data-testid="inherit-date-popover"
+ />
+ <gl-popover
+ :target="() => $refs.epicDatePopover.$el"
+ triggers="focus"
+ placement="left"
+ boundary="viewport"
+ >
+ <p>{{ $options.i18n.dateHelpValidMessage }}</p>
+ <gl-link :href="$options.dateHelpUrl" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </gl-popover>
+ </template>
+ <template #collapsed>
+ <div v-gl-tooltip :title="dateLabel" class="sidebar-collapsed-icon">
+ <gl-icon :size="16" name="calendar" />
+ <span class="collapse-truncated-title">{{ formattedDate }}</span>
+ </div>
+ <sidebar-inherit-date
+ v-if="canInherit"
+ :issuable="issuable"
+ :is-loading="isLoading"
+ :date-type="dateType"
+ @reset-date="setDate(null)"
+ @set-date="setFixedDate"
+ />
+ <sidebar-formatted-date
+ v-else
+ :has-date="hasDate"
+ :formatted-date="formattedDate"
+ :reset-text="removeDateLabel"
+ :is-loading="isLoading"
+ @reset-date="setDate(null)"
+ />
+ </template>
+ <template #default>
+ <gl-datepicker
+ v-if="!isLoading"
+ ref="datePicker"
+ class="gl-relative"
+ :default-date="parsedDate"
+ show-clear-button
+ autocomplete="off"
+ @input="setDate"
+ @clear="setDate(null)"
+ />
+ </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue
new file mode 100644
index 00000000000..87cf1c29fb0
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ inject: ['canUpdate'],
+ props: {
+ formattedDate: {
+ required: true,
+ type: String,
+ },
+ hasDate: {
+ required: true,
+ type: Boolean,
+ },
+ resetText: {
+ required: true,
+ type: String,
+ },
+ isLoading: {
+ required: true,
+ type: Boolean,
+ },
+ canDelete: {
+ required: false,
+ type: Boolean,
+ default: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center hide-collapsed">
+ <span
+ :class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
+ data-testid="sidebar-date-value"
+ >
+ {{ formattedDate }}
+ </span>
+ <div v-if="hasDate && canUpdate && canDelete" class="gl-display-flex">
+ <span class="gl-px-2">-</span>
+ <gl-button
+ variant="link"
+ class="gl-text-gray-500!"
+ data-testid="reset-button"
+ :disabled="isLoading"
+ @click="$emit('reset-date', $event)"
+ >
+ {{ resetText }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
new file mode 100644
index 00000000000..b6bfacb2e47
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_inherit_date.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlFormRadio } from '@gitlab/ui';
+import { dateInWords, parsePikadayDate } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { dateFields } from '../../constants';
+import SidebarFormattedDate from './sidebar_formatted_date.vue';
+
+export default {
+ components: {
+ GlFormRadio,
+ SidebarFormattedDate,
+ },
+ inject: ['canUpdate'],
+ props: {
+ issuable: {
+ required: true,
+ type: Object,
+ },
+ isLoading: {
+ required: true,
+ type: Boolean,
+ },
+ dateType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ dateIsFixed: {
+ get() {
+ return this.issuable?.[dateFields[this.dateType].isDateFixed] || false;
+ },
+ set(fixed) {
+ this.$emit('set-date', fixed);
+ },
+ },
+ hasFixedDate() {
+ return this.issuable[dateFields[this.dateType].dateFixed] !== null;
+ },
+ formattedFixedDate() {
+ const dateFixed = this.issuable[dateFields[this.dateType].dateFixed];
+ if (!dateFixed) {
+ return this.$options.i18n.noDate;
+ }
+
+ return dateInWords(parsePikadayDate(dateFixed), true);
+ },
+ formattedInheritedDate() {
+ const dateFromMilestones = this.issuable[dateFields[this.dateType].dateFromMilestones];
+ if (!dateFromMilestones) {
+ return this.$options.i18n.noDate;
+ }
+
+ return dateInWords(parsePikadayDate(dateFromMilestones), true);
+ },
+ },
+ i18n: {
+ fixed: __('Fixed:'),
+ inherited: __('Inherited:'),
+ remove: __('remove'),
+ noDate: __('None'),
+ },
+};
+</script>
+
+<template>
+ <div class="hide-collapsed gl-mt-3">
+ <div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-fixed-date">
+ <gl-form-radio
+ v-model="dateIsFixed"
+ :value="true"
+ :disabled="!canUpdate || isLoading"
+ class="gl-pr-2"
+ >
+ <span :class="dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
+ {{ $options.i18n.fixed }}
+ </span>
+ </gl-form-radio>
+ <sidebar-formatted-date
+ :has-date="dateIsFixed"
+ :formatted-date="formattedFixedDate"
+ :reset-text="$options.i18n.remove"
+ :is-loading="isLoading"
+ :can-delete="dateIsFixed && hasFixedDate"
+ class="gl-line-height-normal"
+ @reset-date="$emit('reset-date', $event)"
+ />
+ </div>
+ <div class="gl-display-flex gl-align-items-baseline" data-testid="sidebar-inherited-date">
+ <gl-form-radio
+ v-model="dateIsFixed"
+ :value="false"
+ :disabled="!canUpdate || isLoading"
+ class="gl-pr-2"
+ >
+ <span :class="!dateIsFixed ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'">
+ {{ $options.i18n.inherited }}
+ </span>
+ </gl-form-radio>
+ <sidebar-formatted-date
+ :has-date="!dateIsFixed"
+ :formatted-date="formattedInheritedDate"
+ :reset-text="$options.i18n.remove"
+ :is-loading="isLoading"
+ :can-delete="false"
+ class="gl-line-height-normal"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue b/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
deleted file mode 100644
index 141c2b3aae9..00000000000
--- a/app/assets/javascripts/sidebar/components/due_date/sidebar_due_date_widget.vue
+++ /dev/null
@@ -1,203 +0,0 @@
-<script>
-import { GlButton, GlIcon, GlDatepicker, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
-import { IssuableType } from '~/issue_show/constants';
-import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
-import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { dueDateQueries } from '~/sidebar/constants';
-
-const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
- bubbles: true,
-});
-
-export default {
- tracking: {
- event: 'click_edit_button',
- label: 'right_sidebar',
- property: 'dueDate',
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlButton,
- GlIcon,
- GlDatepicker,
- SidebarEditableItem,
- },
- inject: ['fullPath', 'iid', 'canUpdate'],
- props: {
- issuableType: {
- required: true,
- type: String,
- },
- },
- data() {
- return {
- dueDate: null,
- loading: false,
- };
- },
- apollo: {
- dueDate: {
- query() {
- return dueDateQueries[this.issuableType].query;
- },
- variables() {
- return {
- fullPath: this.fullPath,
- iid: String(this.iid),
- };
- },
- update(data) {
- return data.workspace?.issuable?.dueDate || null;
- },
- result({ data }) {
- this.$emit('dueDateUpdated', data.workspace?.issuable?.dueDate);
- },
- error() {
- createFlash({
- message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
- issuableType: this.issuableType,
- }),
- });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.dueDate.loading || this.loading;
- },
- hasDueDate() {
- return this.dueDate !== null;
- },
- parsedDueDate() {
- if (!this.hasDueDate) {
- return null;
- }
-
- return parsePikadayDate(this.dueDate);
- },
- formattedDueDate() {
- if (!this.hasDueDate) {
- return this.$options.i18n.noDueDate;
- }
-
- return dateInWords(this.parsedDueDate, true);
- },
- workspacePath() {
- return this.issuableType === IssuableType.Issue
- ? {
- projectPath: this.fullPath,
- }
- : {
- groupPath: this.fullPath,
- };
- },
- },
- methods: {
- closeForm() {
- this.$refs.editable.collapse();
- this.$el.dispatchEvent(hideDropdownEvent);
- this.$emit('closeForm');
- },
- openDatePicker() {
- this.$refs.datePicker.calendar.show();
- },
- setDueDate(date) {
- this.loading = true;
- this.$refs.editable.collapse();
- this.$apollo
- .mutate({
- mutation: dueDateQueries[this.issuableType].mutation,
- variables: {
- input: {
- ...this.workspacePath,
- iid: this.iid,
- dueDate: date ? formatDate(date, 'yyyy-mm-dd') : null,
- },
- },
- })
- .then(
- ({
- data: {
- issuableSetDueDate: { errors },
- },
- }) => {
- if (errors.length) {
- createFlash({
- message: errors[0],
- });
- } else {
- this.$emit('closeForm');
- }
- },
- )
- .catch(() => {
- createFlash({
- message: sprintf(__('Something went wrong while setting %{issuableType} due date.'), {
- issuableType: this.issuableType,
- }),
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- },
- i18n: {
- dueDate: __('Due date'),
- noDueDate: __('None'),
- removeDueDate: __('remove due date'),
- },
-};
-</script>
-
-<template>
- <sidebar-editable-item
- ref="editable"
- :title="$options.i18n.dueDate"
- :tracking="$options.tracking"
- :loading="isLoading"
- class="block"
- data-testid="due-date"
- @open="openDatePicker"
- >
- <template #collapsed>
- <div v-gl-tooltip :title="$options.i18n.dueDate" class="sidebar-collapsed-icon">
- <gl-icon :size="16" name="calendar" />
- <span class="collapse-truncated-title">{{ formattedDueDate }}</span>
- </div>
- <div class="gl-display-flex gl-align-items-center hide-collapsed">
- <span
- :class="hasDueDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'"
- data-testid="sidebar-duedate-value"
- >
- {{ formattedDueDate }}
- </span>
- <div v-if="hasDueDate && canUpdate" class="gl-display-flex">
- <span class="gl-px-2">-</span>
- <gl-button
- variant="link"
- class="gl-text-gray-500!"
- data-testid="reset-button"
- :disabled="isLoading"
- @click="setDueDate(null)"
- >
- {{ $options.i18n.removeDueDate }}
- </gl-button>
- </div>
- </div>
- </template>
- <template #default>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </template>
- </sidebar-editable-item>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index c3a08f760a0..e85e416881c 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -95,7 +95,7 @@ export default {
<gl-loading-icon v-if="loading" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
- <div v-if="showParticipantLabel" class="title hide-collapsed">
+ <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2">
<gl-loading-icon v-if="loading" :inline="true" />
{{ participantLabel }}
</div>
@@ -105,10 +105,10 @@ export default {
:key="participant.id"
class="participants-author"
>
- <a :href="participant.web_url" class="author-link">
+ <a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image
:lazy="true"
- :img-src="participant.avatar_url"
+ :img-src="participant.avatar_url || participant.avatarUrl"
:size="24"
:tooltip-text="participant.name"
css-classes="avatar-inline"
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
new file mode 100644
index 00000000000..d3043e6f6aa
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -0,0 +1,68 @@
+<script>
+import { __ } from '~/locale';
+import { participantsQueries } from '~/sidebar/constants';
+import Participants from './participants.vue';
+
+export default {
+ i18n: {
+ fetchingError: __('An error occurred while fetching participants'),
+ },
+ components: {
+ Participants,
+ },
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ participants: [],
+ };
+ },
+ apollo: {
+ participants: {
+ query() {
+ return participantsQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.participants.nodes || [];
+ },
+ error(error) {
+ this.$emit('fetch-error', {
+ message: this.$options.i18n.fetchingError,
+ error,
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.participants.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <participants
+ :loading="isLoading"
+ :participants="participants"
+ :number-of-less-participants="7"
+ />
+</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index caf1c92c28a..0fb8d762c7c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -3,6 +3,9 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
+ i18n: {
+ unassigned: __('Unassigned'),
+ },
components: { GlButton, GlLoadingIcon },
inject: {
canUpdate: {},
@@ -40,6 +43,11 @@ export default {
property: null,
}),
},
+ canEdit: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -103,14 +111,16 @@ export default {
<div>
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
<span class="hide-collapsed" data-testid="title" @click="collapse">{{ title }}</span>
+ <slot name="title-extra"></slot>
<gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" />
<gl-loading-icon
v-if="loading && isClassicSidebar"
inline
class="gl-mx-auto gl-my-0 hide-expanded"
/>
+ <slot name="collapsed-right"></slot>
<gl-button
- v-if="canUpdate && !initialLoading"
+ v-if="canUpdate && !initialLoading && canEdit"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button"
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
deleted file mode 100644
index 3ad097138a3..00000000000
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import { deprecatedCreateFlash as Flash } from '../../../flash';
-import { __ } from '../../../locale';
-import Store from '../../stores/sidebar_store';
-import subscriptions from './subscriptions.vue';
-
-export default {
- components: {
- subscriptions,
- },
- props: {
- mediator: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- store: new Store(),
- };
- },
- methods: {
- onToggleSubscription() {
- this.mediator.toggleSubscription().catch(() => {
- Flash(__('Error occurred when toggling the notification subscription'));
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="block subscriptions">
- <subscriptions
- :loading="store.isFetching.subscriptions"
- :project-emails-disabled="store.projectEmailsDisabled"
- :subscribe-disabled-description="store.subscribeDisabledDescription"
- :subscribed="store.subscribed"
- @toggleSubscription="onToggleSubscription"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
new file mode 100644
index 00000000000..ee7502e3457
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -0,0 +1,202 @@
+<script>
+import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { IssuableType } from '~/issue_show/constants';
+import { __, sprintf } from '~/locale';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { subscribedQueries } from '~/sidebar/constants';
+
+const ICON_ON = 'notifications';
+const ICON_OFF = 'notifications-off';
+
+export default {
+ tracking: {
+ event: 'click_edit_button',
+ label: 'right_sidebar',
+ property: 'subscriptions',
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ GlLoadingIcon,
+ GlToggle,
+ SidebarEditableItem,
+ },
+ inject: ['canUpdate'],
+ props: {
+ iid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ subscribed: false,
+ loading: false,
+ emailsDisabled: false,
+ };
+ },
+ apollo: {
+ subscribed: {
+ query() {
+ return subscribedQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.iid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.subscribed || false;
+ },
+ result({ data }) {
+ this.emailsDisabled = this.parentIsGroup
+ ? data.workspace?.emailsDisabled
+ : data.workspace?.issuable?.emailsDisabled;
+ this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
+ },
+ error() {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} notifications.'),
+ {
+ issuableType: this.issuableType,
+ },
+ ),
+ });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries?.subscribed?.loading || this.loading;
+ },
+ notificationTooltip() {
+ if (this.emailsDisabled) {
+ return this.subscribeDisabledDescription;
+ }
+ return this.subscribed ? this.$options.i18n.labelOn : this.$options.i18n.labelOff;
+ },
+ notificationIcon() {
+ if (this.emailsDisabled || !this.subscribed) {
+ return ICON_OFF;
+ }
+ return ICON_ON;
+ },
+ parentIsGroup() {
+ return this.issuableType === IssuableType.Epic;
+ },
+ subscribeDisabledDescription() {
+ return sprintf(__('Disabled by %{parent} owner'), {
+ parent: this.parentIsGroup ? 'group' : 'project',
+ });
+ },
+ },
+ methods: {
+ setSubscribed(subscribed) {
+ this.loading = true;
+ this.$apollo
+ .mutate({
+ mutation: subscribedQueries[this.issuableType].mutation,
+ variables: {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ subscribedState: subscribed,
+ },
+ })
+ .then(
+ ({
+ data: {
+ updateIssuableSubscription: { errors },
+ },
+ }) => {
+ if (errors.length) {
+ createFlash({
+ message: errors[0],
+ });
+ }
+ },
+ )
+ .catch(() => {
+ createFlash({
+ message: sprintf(
+ __('Something went wrong while setting %{issuableType} notifications.'),
+ {
+ issuableType: this.issuableType,
+ },
+ ),
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ toggleSubscribed() {
+ if (this.emailsDisabled) {
+ this.expandSidebar();
+ } else {
+ this.setSubscribed(!this.subscribed);
+ }
+ },
+ expandSidebar() {
+ this.$emit('expandSidebar');
+ },
+ },
+ i18n: {
+ notifications: __('Notifications'),
+ labelOn: __('Notifications on'),
+ labelOff: __('Notifications off'),
+ },
+};
+</script>
+
+<template>
+ <sidebar-editable-item
+ ref="editable"
+ :title="$options.i18n.notifications"
+ :tracking="$options.tracking"
+ :loading="isLoading"
+ :can-edit="false"
+ class="block subscriptions"
+ >
+ <template #collapsed-right>
+ <gl-toggle
+ :value="subscribed"
+ :is-loading="isLoading"
+ :disabled="emailsDisabled || !canUpdate"
+ class="hide-collapsed gl-ml-auto"
+ data-testid="subscription-toggle"
+ :label="$options.i18n.notifications"
+ label-position="hidden"
+ @change="setSubscribed"
+ />
+ </template>
+ <template #collapsed>
+ <span
+ ref="tooltip"
+ v-gl-tooltip.viewport.left
+ :title="notificationTooltip"
+ class="sidebar-collapsed-icon"
+ @click="toggleSubscribed"
+ >
+ <gl-loading-icon v-if="isLoading" class="sidebar-item-icon is-active" />
+ <gl-icon v-else :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
+ </span>
+ <div v-show="emailsDisabled" class="gl-mt-3 hide-collapsed gl-text-gray-500">
+ {{ subscribeDisabledDescription }}
+ </div>
+ </template>
+ <template #default> </template>
+ </sidebar-editable-item>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
new file mode 100644
index 00000000000..67242b3b5b7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import { timelogQueries } from '~/sidebar/constants';
+
+const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ inject: ['issuableId', 'issuableType'],
+ props: {
+ limitToHours: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ data() {
+ return { report: [], isLoading: true };
+ },
+ apollo: {
+ report: {
+ query() {
+ return timelogQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ id: convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId),
+ };
+ },
+ update(data) {
+ this.isLoading = false;
+ return this.extractTimelogs(data);
+ },
+ error() {
+ createFlash({ message: __('Something went wrong. Please try again.') });
+ },
+ },
+ },
+ methods: {
+ isIssue() {
+ return this.issuableType === 'issue';
+ },
+ getGraphQLEntityType() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return this.isIssue() ? 'Issue' : 'MergeRequest';
+ },
+ extractTimelogs(data) {
+ const timelogs = data?.issuable?.timelogs?.nodes || [];
+ return timelogs.slice().sort((a, b) => new Date(a.spentAt) - new Date(b.spentAt));
+ },
+ formatDate(date) {
+ return formatDate(date, TIME_DATE_FORMAT);
+ },
+ getNote(note) {
+ return note?.body;
+ },
+ getTotalTimeSpent() {
+ const seconds = this.report.reduce((acc, item) => acc + item.timeSpent, 0);
+ return this.formatTimeSpent(seconds);
+ },
+ formatTimeSpent(seconds) {
+ const negative = seconds < 0;
+ return (
+ (negative ? '- ' : '') +
+ stringifyTime(parseSeconds(seconds, { limitToHours: this.limitToHours }))
+ );
+ },
+ },
+ fields: [
+ { key: 'spentAt', label: __('Spent At'), sortable: true },
+ { key: 'user', label: __('User'), sortable: true },
+ { key: 'timeSpent', label: __('Time Spent'), sortable: true },
+ { key: 'note', label: __('Note'), sortable: true },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="isLoading"><gl-loading-icon size="md" /></div>
+ <gl-table v-else :items="report" :fields="$options.fields" foot-clone>
+ <template #cell(spentAt)="{ item: { spentAt } }">
+ <div>{{ formatDate(spentAt) }}</div>
+ </template>
+ <template #foot(spentAt)>&nbsp;</template>
+
+ <template #cell(user)="{ item: { user } }">
+ <div>{{ user.name }}</div>
+ </template>
+ <template #foot(user)>&nbsp;</template>
+
+ <template #cell(timeSpent)="{ item: { timeSpent } }">
+ <div>{{ formatTimeSpent(timeSpent) }}</div>
+ </template>
+ <template #foot(timeSpent)>
+ <div>{{ getTotalTimeSpent() }}</div>
+ </template>
+
+ <template #cell(note)="{ item: { note } }">
+ <div>{{ getNote(note) }}</div>
+ </template>
+ <template #foot(note)>&nbsp;</template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 4c095006dd7..64f2ddc1d16 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,10 +1,11 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlLink, GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
import TimeTrackingHelpState from './help_state.vue';
+import TimeTrackingReport from './report.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
export default {
@@ -15,10 +16,16 @@ export default {
},
components: {
GlIcon,
+ GlLink,
+ GlModal,
TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane,
TimeTrackingComparisonPane,
TimeTrackingHelpState,
+ TimeTrackingReport,
+ },
+ directives: {
+ GlModal: GlModalDirective,
},
props: {
timeEstimate: {
@@ -160,6 +167,21 @@ export default {
:time-estimate-human-readable="humanTimeEstimate"
:limit-to-hours="limitToHours"
/>
+ <gl-link
+ v-if="hasTimeSpent"
+ v-gl-modal="'time-tracking-report'"
+ data-testid="reportLink"
+ href="#"
+ class="btn-link"
+ >{{ __('Time tracking report') }}</gl-link
+ >
+ <gl-modal
+ modal-id="time-tracking-report"
+ :title="__('Time tracking report')"
+ :hide-footer="true"
+ >
+ <time-tracking-report :limit-to-hours="limitToHours" />
+ </gl-modal>
<transition name="help-state-toggle">
<time-tracking-help-state v-if="showHelpState" />
</transition>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 80e07d556bf..a4e6d8854d1 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,27 +1,56 @@
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
+import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
+import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
+import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
+import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
+import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
+import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
+import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
+import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
+import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
+import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
-import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
+import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
+import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
+import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
+import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
+import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql';
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
-import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
-import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
+import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
+import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
+import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
export const ASSIGNEES_DEBOUNCE_DELAY = 250;
export const assigneesQueries = {
[IssuableType.Issue]: {
- query: getIssueParticipants,
- mutation: updateAssigneesMutation,
+ query: getIssueAssignees,
+ subscription: issuableAssigneesSubscription,
+ mutation: updateIssueAssigneesMutation,
+ },
+ [IssuableType.MergeRequest]: {
+ query: getMergeRequestAssignees,
+ mutation: updateMergeRequestAssigneesMutation,
+ },
+};
+
+export const participantsQueries = {
+ [IssuableType.Issue]: {
+ query: issueParticipantsQuery,
},
[IssuableType.MergeRequest]: {
query: getMergeRequestParticipants,
- mutation: updateMergeRequestParticipantsMutation,
+ },
+ [IssuableType.Epic]: {
+ query: epicParticipantsQuery,
},
};
@@ -32,7 +61,7 @@ export const confidentialityQueries = {
},
[IssuableType.Epic]: {
query: epicConfidentialQuery,
- mutation: updateEpicMutation,
+ mutation: updateEpicConfidentialMutation,
},
};
@@ -45,9 +74,62 @@ export const referenceQueries = {
},
};
+export const dateTypes = {
+ start: 'startDate',
+ due: 'dueDate',
+};
+
+export const dateFields = {
+ [dateTypes.start]: {
+ isDateFixed: 'startDateIsFixed',
+ dateFixed: 'startDateFixed',
+ dateFromMilestones: 'startDateFromMilestones',
+ },
+ [dateTypes.due]: {
+ isDateFixed: 'dueDateIsFixed',
+ dateFixed: 'dueDateFixed',
+ dateFromMilestones: 'dueDateFromMilestones',
+ },
+};
+
+export const subscribedQueries = {
+ [IssuableType.Issue]: {
+ query: issueSubscribedQuery,
+ mutation: updateIssueSubscriptionMutation,
+ },
+ [IssuableType.Epic]: {
+ query: epicSubscribedQuery,
+ mutation: updateEpicSubscriptionMutation,
+ },
+ [IssuableType.MergeRequest]: {
+ query: mergeRequestSubscribed,
+ mutation: updateMergeRequestSubscriptionMutation,
+ },
+};
+
export const dueDateQueries = {
[IssuableType.Issue]: {
query: issueDueDateQuery,
mutation: updateIssueDueDateMutation,
},
+ [IssuableType.Epic]: {
+ query: epicDueDateQuery,
+ mutation: updateEpicDueDateMutation,
+ },
+};
+
+export const startDateQueries = {
+ [IssuableType.Epic]: {
+ query: epicStartDateQuery,
+ mutation: updateEpicStartDateMutation,
+ },
+};
+
+export const timelogQueries = {
+ [IssuableType.Issue]: {
+ query: getIssueTimelogsQuery,
+ },
+ [IssuableType.MergeRequest]: {
+ query: getMrTimelogsQuery,
+ },
};
diff --git a/app/assets/javascripts/sidebar/fragmentTypes.json b/app/assets/javascripts/sidebar/fragmentTypes.json
new file mode 100644
index 00000000000..a1c68bba454
--- /dev/null
+++ b/app/assets/javascripts/sidebar/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"UNION","name":"Issuable","possibleTypes":[{"name":"Issue"},{"name":"MergeRequest"}]}, {"kind":"INTERFACE","name":"User","possibleTypes":[{"name":"UserCore"}]}]}}
diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js
index aa139540a51..8615b52f1b8 100644
--- a/app/assets/javascripts/sidebar/graphql.js
+++ b/app/assets/javascripts/sidebar/graphql.js
@@ -1,7 +1,21 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
-export const defaultClient = createDefaultClient();
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+export const defaultClient = createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ assumeImmutableResults: true,
+ },
+);
export const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1304e84814b..3f24fdc75dc 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
-import SidebarDueDateWidget from '~/sidebar/components/due_date/sidebar_due_date_widget.vue';
+import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
@@ -24,7 +24,7 @@ import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
-import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
+import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
@@ -53,7 +53,7 @@ function mountAssigneesComponentDeprecated(mediator) {
if (!el) return;
- const { iid, fullPath } = getSidebarOptions();
+ const { id, iid, fullPath } = getSidebarOptions();
const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData();
// eslint-disable-next-line no-new
new Vue({
@@ -74,6 +74,7 @@ function mountAssigneesComponentDeprecated(mediator) {
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
+ issuableId: id,
assigneeAvailabilityStatus,
},
}),
@@ -85,7 +86,7 @@ function mountAssigneesComponent() {
if (!el) return;
- const { iid, fullPath, editable, projectMembersPath } = getSidebarOptions();
+ const { id, iid, fullPath, editable } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
@@ -95,9 +96,7 @@ function mountAssigneesComponent() {
},
provide: {
canUpdate: editable,
- projectMembersPath,
directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
- indirectlyInviteMembers: el.hasAttribute('data-indirectly-invite-members'),
},
render: (createElement) =>
createElement('sidebar-assignees-widget', {
@@ -108,7 +107,8 @@ function mountAssigneesComponent() {
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
- multipleAssignees: !el.dataset.maxAssignees,
+ issuableId: id,
+ allowMultipleAssignees: !el.dataset.maxAssignees,
},
scopedSlots: {
collapsed: ({ users, onClick }) =>
@@ -223,14 +223,14 @@ function mountDueDateComponent() {
SidebarDueDateWidget,
},
provide: {
- iid: String(iid),
- fullPath,
canUpdate: editable,
},
render: (createElement) =>
createElement('sidebar-due-date-widget', {
props: {
+ iid: String(iid),
+ fullPath,
issuableType: IssuableType.Issue,
},
}),
@@ -334,21 +334,32 @@ function mountParticipantsComponent(mediator) {
});
}
-function mountSubscriptionsComponent(mediator) {
+function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
+ const { fullPath, iid, editable } = getSidebarOptions();
+
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
components: {
- sidebarSubscriptions,
+ SidebarSubscriptionsWidget,
+ },
+ provide: {
+ canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-subscriptions', {
+ createElement('sidebar-subscriptions-widget', {
props: {
- mediator,
+ iid: String(iid),
+ fullPath,
+ issuableType:
+ isInIssuePage() || isInIncidentPage() || isInDesignPage()
+ ? IssuableType.Issue
+ : IssuableType.MergeRequest,
},
}),
});
@@ -356,16 +367,16 @@ function mountSubscriptionsComponent(mediator) {
function mountTimeTrackingComponent() {
const el = document.getElementById('issuable-time-tracker');
+ const { id, issuableType } = getSidebarOptions();
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
- components: {
- SidebarTimeTracking,
- },
- render: (createElement) => createElement('sidebar-time-tracking', {}),
+ apolloProvider,
+ provide: { issuableId: id, issuableType },
+ render: (createElement) => createElement(SidebarTimeTracking, {}),
});
}
@@ -425,7 +436,7 @@ export function mountSidebar(mediator) {
mountReferenceComponent(mediator);
mountLockComponent();
mountParticipantsComponent(mediator);
- mountSubscriptionsComponent(mediator);
+ mountSubscriptionsComponent();
mountCopyEmailComponent();
new SidebarMoveIssue(
diff --git a/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql
new file mode 100644
index 00000000000..f60f44abebd
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_due_date.query.graphql
@@ -0,0 +1,13 @@
+query epicDueDate($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ dueDate
+ dueDateIsFixed
+ dueDateFixed
+ dueDateFromMilestones
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
new file mode 100644
index 00000000000..fbebc50ab08
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
@@ -0,0 +1,18 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query epicParticipants($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ participants {
+ nodes {
+ ...User
+ ...UserAvailability
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql
new file mode 100644
index 00000000000..c6c24fd3d95
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_start_date.query.graphql
@@ -0,0 +1,13 @@
+query epicStartDate($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ startDate
+ startDateIsFixed
+ startDateFixed
+ startDateFromMilestones
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
new file mode 100644
index 00000000000..9f1967e1685
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_subscribed.query.graphql
@@ -0,0 +1,11 @@
+query epicSubscribed($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ emailsDisabled
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ subscribed
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
new file mode 100644
index 00000000000..47ce094418c
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription issuableAssigneesUpdated($issuableId: IssuableID!) {
+ issuableAssigneesUpdated(issuableId: $issuableId) {
+ ... on Issue {
+ assignees {
+ nodes {
+ ...User
+ status {
+ availability
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
new file mode 100644
index 00000000000..7d38b5d3bd8
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issue_subscribed.query.graphql
@@ -0,0 +1,11 @@
+query issueSubscribed($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: issue(iid: $iid) {
+ __typename
+ id
+ subscribed
+ emailsDisabled
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
new file mode 100644
index 00000000000..3b54a2e529b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/merge_request_subscribed.query.graphql
@@ -0,0 +1,10 @@
+query mergeRequestSubscribed($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: mergeRequest(iid: $iid) {
+ __typename
+ id
+ subscribed
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql
new file mode 100644
index 00000000000..9b0a8b4a8f7
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_epic_due_date.mutation.graphql
@@ -0,0 +1,11 @@
+mutation updateEpicDueDate($input: UpdateEpicInput!) {
+ issuableSetDate: updateEpic(input: $input) {
+ issuable: epic {
+ id
+ dueDateIsFixed
+ dueDateFixed
+ dueDateFromMilestones
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql
new file mode 100644
index 00000000000..9b4bb9159c3
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_epic_start_date.mutation.graphql
@@ -0,0 +1,11 @@
+mutation updateEpicStartDate($input: UpdateEpicInput!) {
+ issuableSetDate: updateEpic(input: $input) {
+ issuable: epic {
+ id
+ startDateIsFixed
+ startDateFixed
+ startDateFromMilestones
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
index f2b806102f4..af43766aed5 100644
--- a/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_epic_subscription.mutation.graphql
@@ -1,6 +1,9 @@
-mutation epicSetSubscription($input: EpicSetSubscriptionInput!) {
- updateIssuableSubscription: epicSetSubscription(input: $input) {
- epic {
+mutation epicSetSubscription($fullPath: ID!, $iid: ID!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: epicSetSubscription(
+ input: { groupPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: epic {
+ id
subscribed
}
errors
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
index cf7eccd61c7..4765b0b08cc 100644
--- a/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_due_date.mutation.graphql
@@ -1,5 +1,5 @@
mutation updateIssueDueDate($input: UpdateIssueInput!) {
- issuableSetDueDate: updateIssue(input: $input) {
+ issuableSetDate: updateIssue(input: $input) {
issuable: issue {
id
dueDate
diff --git a/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
new file mode 100644
index 00000000000..81891fb601f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_issue_subscription.mutation.graphql
@@ -0,0 +1,11 @@
+mutation issueSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: issueSetSubscription(
+ input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: issue {
+ id
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql
new file mode 100644
index 00000000000..69944ff9a13
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_subscription.mutation.graphql
@@ -0,0 +1,11 @@
+mutation mergeRequestSetSubscription($fullPath: ID!, $iid: String!, $subscribedState: Boolean!) {
+ updateIssuableSubscription: mergeRequestSetSubscription(
+ input: { projectPath: $fullPath, iid: $iid, subscribedState: $subscribedState }
+ ) {
+ issuable: mergeRequest {
+ id
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index 5fb20b00705..b08bf26e1dc 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -1,7 +1,6 @@
import { s__, __ } from '~/locale';
export const BRANCH_SUFFIX_COUNT = 8;
-export const DEFAULT_TARGET_BRANCH = 'master';
export const ISSUABLE_TYPE = 'merge_request';
export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.');
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
index 0b74c99b319..e9f1828bff8 100644
--- a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
@@ -9,6 +9,7 @@ const submitContentChangesResolver = (
project: projectId,
username,
sourcePath,
+ targetBranch,
content,
images,
mergeRequestMeta,
@@ -21,6 +22,7 @@ const submitContentChangesResolver = (
projectId,
username,
sourcePath,
+ targetBranch,
content,
images,
mergeRequestMeta,
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 22f80ead74b..49a2ca03ace 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -111,6 +111,7 @@ export default {
project: this.appData.project,
username: this.appData.username,
sourcePath: this.appData.sourcePath,
+ targetBranch: this.appData.branch,
content: this.content,
formattedMarkdown: this.formattedMarkdown,
images: this.images,
diff --git a/app/assets/javascripts/static_site_editor/services/generate_branch_name.js b/app/assets/javascripts/static_site_editor/services/generate_branch_name.js
index f45ad616332..cbf03a41ce2 100644
--- a/app/assets/javascripts/static_site_editor/services/generate_branch_name.js
+++ b/app/assets/javascripts/static_site_editor/services/generate_branch_name.js
@@ -1,8 +1,8 @@
-import { BRANCH_SUFFIX_COUNT, DEFAULT_TARGET_BRANCH } from '../constants';
+import { BRANCH_SUFFIX_COUNT } from '../constants';
const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT);
-const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) =>
+const generateBranchName = (username, targetBranch) =>
`${username}-${targetBranch}-patch-${generateBranchSuffix()}`;
export default generateBranchName;
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index 6391cfd6cc2..ecb7f60a421 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -4,7 +4,6 @@ import generateBranchName from '~/static_site_editor/services/generate_branch_na
import Tracking from '~/tracking';
import {
- DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
@@ -16,9 +15,9 @@ import {
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '../constants';
-const createBranch = (projectId, branch) =>
+const createBranch = (projectId, branch, targetBranch) =>
Api.createBranch(projectId, {
- ref: DEFAULT_TARGET_BRANCH,
+ ref: targetBranch,
branch,
}).catch(() => {
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
@@ -73,13 +72,7 @@ const commit = (projectId, message, branch, actions) => {
});
};
-const createMergeRequest = (
- projectId,
- title,
- description,
- sourceBranch,
- targetBranch = DEFAULT_TARGET_BRANCH,
-) => {
+const createMergeRequest = (projectId, title, description, sourceBranch, targetBranch) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST);
Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
@@ -100,16 +93,17 @@ const submitContentChanges = ({
username,
projectId,
sourcePath,
+ targetBranch,
content,
images,
mergeRequestMeta,
formattedMarkdown,
}) => {
- const branch = generateBranchName(username);
+ const branch = generateBranchName(username, targetBranch);
const { title: mergeRequestTitle, description: mergeRequestDescription } = mergeRequestMeta;
const meta = {};
- return createBranch(projectId, branch)
+ return createBranch(projectId, branch, targetBranch)
.then(({ data: { web_url: url } }) => {
const message = `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`;
@@ -133,7 +127,13 @@ const submitContentChanges = ({
.then(({ data: { short_id: label, web_url: url } }) => {
Object.assign(meta, { commit: { label, url } });
- return createMergeRequest(projectId, mergeRequestTitle, mergeRequestDescription, branch);
+ return createMergeRequest(
+ projectId,
+ mergeRequestTitle,
+ mergeRequestDescription,
+ branch,
+ targetBranch,
+ );
})
.then(({ data: { iid: label, web_url: url } }) => {
Object.assign(meta, { mergeRequest: { label: label.toString(), url } });
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index bc8a8e425dd..3b2210b9ef2 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -40,20 +40,37 @@ export default class TaskList {
taskListField.value = taskListField.dataset.value;
});
- $(this.taskListContainerSelector).taskList('enable');
- $(document).on('tasklist:changed', this.taskListContainerSelector, this.updateHandler);
+ this.enable();
}
getTaskListTarget(e) {
return e && e.currentTarget ? $(e.currentTarget) : $(this.taskListContainerSelector);
}
+ // Disable any task items that don't have a data-sourcepos attribute, on the
+ // assumption that if it doesn't then it wasn't generated from our markdown parser.
+ // This covers the case of markdown not being able to handle task lists inside
+ // markdown tables. It also includes hand coded HTML lists.
+ disableNonMarkdownTaskListItems(e) {
+ this.getTaskListTarget(e)
+ .find('.task-list-item')
+ .not('[data-sourcepos]')
+ .find('.task-list-item-checkbox')
+ .prop('disabled', true);
+ }
+
disableTaskListItems(e) {
this.getTaskListTarget(e).taskList('disable');
}
enableTaskListItems(e) {
this.getTaskListTarget(e).taskList('enable');
+ this.disableNonMarkdownTaskListItems(e);
+ }
+
+ enable() {
+ this.enableTaskListItems();
+ $(document).on('tasklist:changed', this.taskListContainerSelector, this.updateHandler);
}
disable() {
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index cdfecceb78a..d2e69bc06cf 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -153,6 +153,21 @@ export default class Tracking {
return loadEvents;
}
+ static enableFormTracking(config, contexts = []) {
+ if (!this.enabled()) return;
+
+ if (!config?.forms?.whitelist?.length && !config?.fields?.whitelist?.length) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unable to enable form event tracking without whitelist rules.');
+ }
+
+ contexts.unshift(STANDARD_CONTEXT);
+ const enabler = () => window.snowplow('enableFormTracking', config, contexts);
+
+ if (document.readyState !== 'loading') enabler();
+ else document.addEventListener('DOMContentLoaded', enabler);
+ }
+
static mixin(opts = {}) {
return {
computed: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index abc831c8abe..a5d165ebd49 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlLink,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
@@ -9,6 +16,7 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
+ GlIcon,
GlLink,
GlSearchBoxByType,
ReviewAppLink,
@@ -71,7 +79,14 @@ export default {
size="small"
css-class="deploy-link js-deploy-url inline"
/>
- <gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
+ <gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
+ <template #button-content>
+ <gl-icon
+ class="dropdown-chevron gl-mx-0!"
+ name="chevron-down"
+ data-testid="mr-wigdet-deployment-dropdown-icon"
+ />
+ </template>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="change in filteredChanges"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 1248a891ed9..fa46b4b1364 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -107,9 +107,6 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
- hasArtifacts() {
- return this.pipeline?.details?.artifacts?.length > 0;
- },
isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
},
@@ -288,11 +285,7 @@ export default {
/>
</span>
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
- <pipeline-artifacts
- v-if="hasArtifacts"
- :artifacts="pipeline.details.artifacts"
- class="gl-ml-3"
- />
+ <pipeline-artifacts :pipeline-id="pipeline.id" class="gl-ml-3" />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 6d68c15cf2d..0cd280c42d2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -154,7 +154,7 @@ export default {
<status-icon status="success" />
<div class="media-body">
<h4 class="gl-display-flex">
- <span class="gl-mr-3" data-qa-selector="merge_request_status_content">
+ <span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
@@ -169,6 +169,7 @@ export default {
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
+ data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
index 0655eef6504..32749b8b018 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -1,4 +1,5 @@
<script>
+import { MERGE_ACTIVE_STATUS_PHRASES } from '../../constants';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -13,13 +14,23 @@ export default {
default: () => ({}),
},
},
+ data() {
+ const statusCount = MERGE_ACTIVE_STATUS_PHRASES.length;
+
+ return {
+ mergeStatus: MERGE_ACTIVE_STATUS_PHRASES[Math.floor(Math.random() * statusCount)],
+ };
+ },
};
</script>
<template>
<div class="mr-widget-body mr-state-locked media">
<status-icon status="loading" />
<div class="media-body">
- <h4>{{ s__('mrWidget|This merge request is in the process of being merged') }}</h4>
+ <h4>
+ {{ mergeStatus.message }}
+ <gl-emoji :data-name="mergeStatus.emoji" />
+ </h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 33ca582583b..a82a8a22873 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -175,7 +175,7 @@ export default {
>
<gl-button
:loading="isMakingRequest"
- variant="success"
+ variant="confirm"
data-qa-selector="mr_rebase_button"
@click="rebase"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 329964d009a..c6ce29acb09 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -28,7 +28,7 @@ export default {
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body">
<span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{
- s__('mrWidget|Before this can be merged, one or more threads must be resolved.')
+ s__('mrWidget|Merge blocked: all threads must be resolved.')
}}</span>
<gl-button
data-testid="jump-to-first"
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 77dfbf9d385..822fb58db60 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -25,3 +25,30 @@ export const SP_HELP_CONTENT = s__(
);
export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/';
export const SP_ICON_NAME = 'status_notfound';
+
+export const MERGE_ACTIVE_STATUS_PHRASES = [
+ {
+ message: s__('mrWidget|Merging! Drum roll, please…'),
+ emoji: 'drum',
+ },
+ {
+ message: s__("mrWidget|Merging! We're almost there…"),
+ emoji: 'sparkles',
+ },
+ {
+ message: s__('mrWidget|Merging! Changes will land soon…'),
+ emoji: 'airplane_arriving',
+ },
+ {
+ message: s__('mrWidget|Merging! Changes are being shipped…'),
+ emoji: 'ship',
+ },
+ {
+ message: s__("mrWidget|Merging! Everything's good…"),
+ emoji: 'relieved',
+ },
+ {
+ message: s__('mrWidget|Merging! This is going to be great…'),
+ emoji: 'heart_eyes',
+ },
+];
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index c1c491f6fe0..3a3a1329483 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -32,6 +32,10 @@ export default () => {
const vm = new Vue({
el: '#js-vue-mr-widget',
+ provide: {
+ artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
+ artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
+ },
...MrWidgetOptions,
apolloProvider,
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js
index 23e140623cc..67d9892d9c6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js
@@ -9,7 +9,7 @@ export default {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
},
cancelButtonText() {
- return s__('mrWidget|Cancel automatic merge');
+ return s__('mrWidget|Cancel');
},
},
};
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 264ea36137f..0cfb059b0ce 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
@@ -460,9 +460,6 @@ export default {
<grouped-codequality-reports-app
v-if="shouldRenderCodeQuality"
:base-path="mr.codeclimate.base_path"
- :head-path="mr.codeclimate.head_path"
- :head-blob-path="mr.headBlobPath"
- :base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath"
/>
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 f57b638dd81..9f85140bab8 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
@@ -1,6 +1,6 @@
import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import mrEventHub from '~/merge_request/eventhub';
+import { statusBoxState } from '~/issuable/components/status_box.vue';
import { formatDate } from '../../lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
import { stateKey } from './state_maps';
@@ -23,6 +23,8 @@ export default class MergeRequestStore {
setData(data, isRebased) {
this.initApprovals();
+ this.updateStatusState(data.state);
+
if (isRebased) {
this.sha = data.diff_head_sha;
}
@@ -156,16 +158,14 @@ export default class MergeRequestStore {
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState();
-
- if (!window.gon?.features?.mergeRequestWidgetGraphql) {
- this.emitUpdatedState();
- }
}
setGraphqlData(project) {
const { mergeRequest } = project;
const pipeline = mergeRequest.headPipeline;
+ this.updateStatusState(mergeRequest.state);
+
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
@@ -190,10 +190,15 @@ export default class MergeRequestStore {
this.workInProgress = mergeRequest.workInProgress;
this.mergeRequestState = mergeRequest.state;
- this.emitUpdatedState();
this.setState();
}
+ updateStatusState(state) {
+ if (this.mergeRequestState !== state && statusBoxState.updateStatus) {
+ statusBoxState.updateStatus();
+ }
+ }
+
setState() {
if (this.mergeOngoing) {
this.state = 'merging';
@@ -216,12 +221,6 @@ export default class MergeRequestStore {
}
}
- emitUpdatedState() {
- mrEventHub.$emit('mr.state.updated', {
- state: this.mergeRequestState,
- });
- }
-
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
index 554c7a573fe..ca42cb0b1b5 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_sidebar.vue
@@ -64,6 +64,9 @@ export default {
<sidebar-status
:project-path="projectPath"
:alert="alert"
+ :sidebar-collapsed="sidebarStatus"
+ text-class="gl-text-gray-500"
+ class="gl-w-70p"
@toggle-sidebar="$emit('toggle-sidebar')"
@alert-error="$emit('alert-error', $event)"
/>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2a999b908f9..ef31106b709 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -192,21 +192,33 @@ export default {
</script>
<template>
- <div class="block alert-assignees">
- <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
- <gl-icon name="user" :size="14" />
- <gl-loading-icon v-if="isUpdating" />
- </div>
- <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
- <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
- <template #assignees>
- {{ userName }}
- </template>
- </gl-sprintf>
- </gl-tooltip>
+ <div
+ class="alert-assignees gl-py-5 gl-w-70p"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }"
+ >
+ <template v-if="sidebarCollapsed">
+ <div
+ ref="assignees"
+ class="gl-mb-6 gl-ml-6"
+ data-testid="assignees-icon"
+ @click="$emit('toggle-sidebar')"
+ >
+ <gl-icon name="user" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
+ <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
+ <template #assignees>
+ {{ userName }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </template>
- <div class="hide-collapsed">
- <p class="title gl-display-flex gl-justify-content-space-between">
+ <div v-else>
+ <p
+ class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
+ >
{{ __('Assignee') }}
<a
v-if="isEditable"
@@ -264,7 +276,11 @@ export default {
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
- <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
+ <div
+ v-else-if="!isDropdownShowing"
+ class="hide-collapsed value gl-m-0"
+ :class="{ 'no-value': !userName }"
+ >
<div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
<span class="gl-relative gl-mr-4">
<img
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index fd40b5d9f65..832b154b312 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -25,7 +25,7 @@ export default {
</script>
<template>
- <div class="block gl-display-flex gl-justify-content-space-between">
+ <div class="block gl-display-flex gl-justify-content-space-between gl-border-b-gray-100!">
<span class="issuable-header-text hide-collapsed">
{{ __('To Do') }}
</span>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index 3822b9153a4..8715eb99518 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -30,6 +30,15 @@ export default {
required: false,
default: true,
},
+ sidebarCollapsed: {
+ type: Boolean,
+ required: false,
+ },
+ textClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -48,34 +57,44 @@ export default {
},
toggleFormDropdown() {
this.isDropdownShowing = !this.isDropdownShowing;
- const { dropdown } = this.$children[2].$refs.dropdown.$refs;
+ const { dropdown } = this.$refs.status.$refs.dropdown.$refs;
if (dropdown && this.isDropdownShowing) {
dropdown.show();
}
},
- handleUpdating(updating) {
- this.isUpdating = updating;
+ handleUpdating(isMutationInProgress) {
+ if (!isMutationInProgress) {
+ this.$emit('alert-update');
+ }
+ this.isUpdating = isMutationInProgress;
},
},
};
</script>
<template>
- <div class="block alert-status">
- <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')">
- <gl-icon name="status" :size="14" />
- <gl-loading-icon v-if="isUpdating" />
- </div>
- <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
- <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
- <template #status>
- {{ alert.status.toLowerCase() }}
- </template>
- </gl-sprintf>
- </gl-tooltip>
+ <div
+ class="alert-status gl-py-5"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': !sidebarCollapsed }"
+ >
+ <template v-if="sidebarCollapsed">
+ <div ref="status" class="gl-ml-6" data-testid="status-icon" @click="$emit('toggle-sidebar')">
+ <gl-icon name="status" />
+ <gl-loading-icon v-if="isUpdating" />
+ </div>
+ <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
+ <gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
+ <template #status>
+ {{ alert.status.toLowerCase() }}
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </template>
- <div class="hide-collapsed">
- <p class="title gl-display-flex justify-content-between">
+ <div v-else>
+ <p
+ class="gl-text-gray-900 gl-mb-2 gl-line-height-20 gl-display-flex gl-justify-content-space-between"
+ >
{{ s__('AlertManagement|Status') }}
<a
v-if="isEditable"
@@ -90,6 +109,7 @@ export default {
</p>
<alert-status
+ ref="status"
:alert="alert"
:project-path="projectPath"
:is-dropdown-showing="isDropdownShowing"
@@ -106,7 +126,7 @@ export default {
class="value gl-m-0"
:class="{ 'no-value': !statuses[alert.status] }"
>
- <span v-if="statuses[alert.status]" class="gl-text-gray-500" data-testid="status">
+ <span v-if="statuses[alert.status]" :class="textClass" data-testid="status">
{{ statuses[alert.status] }}
</span>
<span v-else>
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
index 271f0b4e4bb..a2a4046ab81 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_todo.vue
@@ -134,7 +134,12 @@ export default {
</script>
<template>
- <div :class="{ 'block todo': sidebarCollapsed, 'gl-ml-auto': !sidebarCollapsed }">
+ <div
+ :class="{
+ 'block todo': sidebarCollapsed,
+ 'gl-ml-auto': !sidebarCollapsed,
+ }"
+ >
<todo
data-testid="alert-todo-button"
:collapsed="sidebarCollapsed"
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
index bc4d91a51d1..f0095abfca1 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql
@@ -3,6 +3,7 @@ mutation createAlertIssue($projectPath: ID!, $iid: String!) {
errors
issue {
iid
+ webUrl
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue b/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
new file mode 100644
index 00000000000..1f293b2150f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/alerts_deprecation_warning.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['hasManagedPrometheus'],
+ i18n: {
+ alertsDeprecationText: s__(
+ 'Metrics|GitLab-managed Prometheus is deprecated and %{linkStart}scheduled for removal%{linkEnd}. Following this removal, your existing alerts will continue to function as part of the new cluster integration. However, you will no longer be able to add new alerts or edit existing alerts from the metrics dashboard.',
+ ),
+ },
+ methods: {
+ helpPagePath,
+ },
+};
+</script>
+
+<template>
+ <gl-alert v-if="hasManagedPrometheus" variant="warning" class="my-2">
+ <gl-sprintf :message="$options.i18n.alertsDeprecationText">
+ <template #link="{ content }">
+ <gl-link
+ :href="
+ helpPagePath('operations/metrics/alerts.html', {
+ anchor: 'managed-prometheus-instances',
+ })
+ "
+ target="_blank"
+ >
+ <span>{{ content }}</span>
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index f477610ff1d..f6ab3cac536 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -6,6 +6,7 @@ import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
export default {
+ name: 'SimpleViewer',
components: {
GlIcon,
EditorLite: () =>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
index ad3e6713e45..2552236a073 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
@@ -1,7 +1,7 @@
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
-import { CHART_CONTAINER_HEIGHT } from '../constants';
+import { CHART_CONTAINER_HEIGHT } from './constants';
export default {
name: 'CiCdAnalyticsAreaChart',
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index f4fd57e4cdc..f4fd57e4cdc 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js
new file mode 100644
index 00000000000..1561674c0ad
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js
@@ -0,0 +1 @@
+export const CHART_CONTAINER_HEIGHT = 300;
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index eb8400e81c7..a1c7c4dd142 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -28,6 +28,7 @@ export default {
</script>
<template>
+ <!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
:disabled="isDisabled || isLoading"
class="dropdown-menu-toggle dropdown-menu-full-width"
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index e622b505570..e1e71639115 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -93,6 +93,7 @@ const fileExtensionIcons = {
pdf: 'pdf',
xlsx: 'table',
xls: 'table',
+ ods: 'table',
csv: 'table',
tsv: 'table',
vscodeignore: 'vscode',
@@ -154,6 +155,7 @@ const fileExtensionIcons = {
gradle: 'gradle',
doc: 'word',
docx: 'word',
+ odt: 'word',
rtf: 'word',
cer: 'certificate',
cert: 'certificate',
@@ -204,6 +206,7 @@ const fileExtensionIcons = {
pps: 'powerpoint',
ppam: 'powerpoint',
ppa: 'powerpoint',
+ odp: 'powerpoint',
webm: 'movie',
mkv: 'movie',
flv: 'movie',
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 3d8afd162cb..2cb1b6a195f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -1,24 +1,46 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
-const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
-export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
-export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
+export const DEBOUNCE_DELAY = 200;
+export const MAX_RECENT_TOKENS_SIZE = 3;
-export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
+export const FILTER_NONE = 'None';
+export const FILTER_ANY = 'Any';
+export const FILTER_CURRENT = 'Current';
-export const DEBOUNCE_DELAY = 200;
+export const OPERATOR_IS = '=';
+export const OPERATOR_IS_TEXT = __('is');
+export const OPERATOR_IS_NOT = '!=';
+
+export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
+
+export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) };
+export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) };
+export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+
+export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
+ { value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
+]);
+
+export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; // eslint-disable-line @gitlab/require-i18n-strings
+
+export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
+ { value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings
+ { value: 'Started', text: __('Started') }, // eslint-disable-line @gitlab/require-i18n-strings
+]);
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
};
-export const DEFAULT_MILESTONES = [
- DEFAULT_LABEL_NONE,
- DEFAULT_LABEL_ANY,
- { value: 'Upcoming', text: __('Upcoming') },
- { value: 'Started', text: __('Started') },
-];
+export const FILTERED_SEARCH_TERM = 'filtered-search-term';
-/* eslint-enable @gitlab/require-i18n-strings */
+export const TOKEN_TITLE_AUTHOR = __('Author');
+export const TOKEN_TITLE_ASSIGNEE = __('Assignee');
+export const TOKEN_TITLE_MILESTONE = __('Milestone');
+export const TOKEN_TITLE_LABEL = __('Label');
+export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
+export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
+export const TOKEN_TITLE_ITERATION = __('Iteration');
+export const TOKEN_TITLE_EPIC = __('Epic');
+export const TOKEN_TITLE_WEIGHT = __('Weight');
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 107ced550c1..3e7feb91b27 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -93,9 +93,9 @@ export default {
sortBy.sortDirection.descending === this.initialSortBy,
)
.pop();
- selectedSortDirection = this.initialSortBy.endsWith('_desc')
- ? SortDirection.descending
- : SortDirection.ascending;
+ selectedSortDirection = Object.keys(selectedSortOption.sortDirection).find(
+ (key) => selectedSortOption.sortDirection[key] === this.initialSortBy,
+ );
}
return {
@@ -324,7 +324,9 @@ export default {
class="gl-align-self-center"
:checked="checkboxChecked"
@input="$emit('checked-input', $event)"
- />
+ >
+ <span class="gl-sr-only">{{ __('Select all') }}</span>
+ </gl-form-checkbox>
<gl-filtered-search
ref="filteredSearchInput"
v-model="filterValue"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index a15cf220ee5..e5c8d29e09b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -1,6 +1,9 @@
-import { isEmpty } from 'lodash';
+import { isEmpty, uniqWith, isEqual } from 'lodash';
+import AccessorUtilities from '~/lib/utils/accessor';
import { queryToObject } from '~/lib/utils/url_utility';
+import { MAX_RECENT_TOKENS_SIZE } from './constants';
+
/**
* Strips enclosing quotations from a string if it has one.
*
@@ -162,3 +165,38 @@ export function urlQueryToFilter(query = '') {
return { ...memo, [filterName]: { value, operator } };
}, {});
}
+
+/**
+ * Returns array of token values from localStorage
+ * based on provided recentTokenValuesStorageKey
+ *
+ * @param {String} recentTokenValuesStorageKey
+ * @returns
+ */
+export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) {
+ let recentlyUsedTokenValues = [];
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || [];
+ }
+ return recentlyUsedTokenValues;
+}
+
+/**
+ * Sets provided token value to recently used array
+ * within localStorage for provided recentTokenValuesStorageKey
+ *
+ * @param {String} recentTokenValuesStorageKey
+ * @param {Object} tokenValue
+ */
+export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) {
+ const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey);
+
+ recentlyUsedTokenValues.splice(0, 0, { ...tokenValue });
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(
+ recentTokenValuesStorageKey,
+ JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
+ );
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
new file mode 100644
index 00000000000..6ebc5431012
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -0,0 +1,167 @@
+<script>
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+
+import { DEBOUNCE_DELAY } from '../constants';
+import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ },
+ props: {
+ tokenConfig: {
+ type: Object,
+ required: true,
+ },
+ tokenValue: {
+ type: Object,
+ required: true,
+ },
+ tokenActive: {
+ type: Boolean,
+ required: true,
+ },
+ tokensListLoading: {
+ type: Boolean,
+ required: true,
+ },
+ tokenValues: {
+ type: Array,
+ required: true,
+ },
+ fnActiveTokenValue: {
+ type: Function,
+ required: true,
+ },
+ defaultTokenValues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ recentTokenValuesStorageKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ valueIdentifier: {
+ type: String,
+ required: false,
+ default: 'id',
+ },
+ fnCurrentTokenValue: {
+ type: Function,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchKey: '',
+ recentTokenValues: this.recentTokenValuesStorageKey
+ ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey)
+ : [],
+ loading: false,
+ };
+ },
+ computed: {
+ isRecentTokenValuesEnabled() {
+ return Boolean(this.recentTokenValuesStorageKey);
+ },
+ recentTokenIds() {
+ return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name);
+ },
+ currentTokenValue() {
+ if (this.fnCurrentTokenValue) {
+ return this.fnCurrentTokenValue(this.tokenValue.data);
+ }
+ return this.tokenValue.data.toLowerCase();
+ },
+ activeTokenValue() {
+ return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue);
+ },
+ /**
+ * Return all the tokenValues when searchKey is present
+ * otherwise return only the tokenValues which aren't
+ * present in "Recently used"
+ */
+ availableTokenValues() {
+ return this.searchKey
+ ? this.tokenValues
+ : this.tokenValues.filter(
+ (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]),
+ );
+ },
+ },
+ watch: {
+ tokenActive: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.tokenValues.length) {
+ this.$emit('fetch-token-values', this.tokenValue.data);
+ }
+ },
+ },
+ },
+ methods: {
+ handleInput({ data }) {
+ this.searchKey = data;
+ setTimeout(() => {
+ if (!this.tokensListLoading) this.$emit('fetch-token-values', data);
+ }, DEBOUNCE_DELAY);
+ },
+ handleTokenValueSelected(activeTokenValue) {
+ if (this.isRecentTokenValuesEnabled) {
+ setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="tokenConfig"
+ v-bind="{ ...this.$parent.$props, ...this.$parent.$attrs }"
+ v-on="this.$parent.$listeners"
+ @input="handleInput"
+ @select="handleTokenValueSelected(activeTokenValue)"
+ >
+ <template #view-token="viewTokenProps">
+ <slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ </template>
+ <template #view="viewTokenProps">
+ <slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
+ </template>
+ <template #suggestions>
+ <template v-if="defaultTokenValues.length">
+ <gl-filtered-search-suggestion
+ v-for="token in defaultTokenValues"
+ :key="token.value"
+ :value="token.value"
+ >
+ {{ token.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider />
+ </template>
+ <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey">
+ <gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
+ <slot name="token-values-list" :token-values="recentTokenValues"></slot>
+ <gl-dropdown-divider />
+ </template>
+ <gl-loading-icon v-if="tokensListLoading" />
+ <template v-else>
+ <slot name="token-values-list" :token-values="availableTokenValues"></slot>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 98190d716c9..f2f4787d80b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -10,7 +10,7 @@ import { debounce } from 'lodash';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
-import { DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants';
+import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
@@ -33,7 +33,7 @@ export default {
data() {
return {
emojis: this.config.initialEmojis || [],
- defaultEmojis: this.config.defaultEmojis || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY],
+ defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY,
loading: true,
};
},
@@ -47,6 +47,16 @@ export default {
);
},
},
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.emojis.length) {
+ this.fetchEmojiBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
methods: {
fetchEmojiBySearchTerm(searchTerm) {
this.loading = true;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
index 101c7150c55..1450807b11d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
@@ -1,15 +1,18 @@
<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import { debounce } from 'lodash';
-
import createFlash from '~/flash';
-import { isNumeric } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
-import { DEBOUNCE_DELAY } from '../constants';
-import { stripQuotes } from '../filtered_search_utils';
+import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
export default {
components: {
+ GlDropdownDivider,
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlLoadingIcon,
@@ -32,29 +35,16 @@ export default {
},
computed: {
currentValue() {
- /*
- * When the URL contains the epic_iid, we'd get: '123'
- */
- if (isNumeric(this.value.data)) {
- return parseInt(this.value.data, 10);
- }
-
- /*
- * When the token is added in current session it'd be: 'Foo::&123'
- */
- const id = this.value.data.split('::&')[1];
-
- if (id) {
- return parseInt(id, 10);
- }
-
- return this.value.data;
+ return Number(this.value.data);
+ },
+ defaultEpics() {
+ return this.config.defaultEpics || DEFAULT_NONE_ANY;
+ },
+ idProperty() {
+ return this.config.idProperty || 'id';
},
activeEpic() {
- const currentValueIsString = typeof this.currentValue === 'string';
- return this.epics.find(
- (epic) => epic[currentValueIsString ? 'title' : 'iid'] === this.currentValue,
- );
+ return this.epics.find((epic) => epic[this.idProperty] === this.currentValue);
},
},
watch: {
@@ -72,20 +62,8 @@ export default {
this.loading = true;
this.config
.fetchEpics(searchTerm)
- .then(({ data }) => {
- this.epics = data;
- })
- .catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
- .finally(() => {
- this.loading = false;
- });
- },
- fetchSingleEpic(iid) {
- this.loading = true;
- this.config
- .fetchSingleEpic(iid)
- .then(({ data }) => {
- this.epics = [data];
+ .then((response) => {
+ this.epics = Array.isArray(response) ? response : response.data;
})
.catch(() => createFlash({ message: __('There was a problem fetching epics.') }))
.finally(() => {
@@ -93,17 +71,13 @@ export default {
});
},
searchEpics: debounce(function debouncedSearch({ data }) {
- if (isNumeric(data)) {
- return this.fetchSingleEpic(data);
- }
- return this.fetchEpicsBySearchTerm(data);
+ this.fetchEpicsBySearchTerm(data);
}, DEBOUNCE_DELAY),
- getEpicValue(epic) {
- return `${epic.title}::&${epic.iid}`;
+ getEpicDisplayText(epic) {
+ return `${epic.title}::&${epic[this.idProperty]}`;
},
},
- stripQuotes,
};
</script>
@@ -115,17 +89,25 @@ export default {
@input="searchEpics"
>
<template #view="{ inputValue }">
- <span>{{ activeEpic ? getEpicValue(activeEpic) : $options.stripQuotes(inputValue) }}</span>
+ {{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }}
</template>
<template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="epic in defaultEpics"
+ :key="epic.value"
+ :value="epic.value"
+ >
+ {{ epic.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultEpics.length" />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
v-for="epic in epics"
- :key="epic.id"
- :value="getEpicValue(epic)"
+ :key="epic[idProperty]"
+ :value="String(epic[idProperty])"
>
- <div>{{ epic.title }}</div>
+ {{ epic.title }}
</gl-filtered-search-suggestion>
</template>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
new file mode 100644
index 00000000000..7b6a590279a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -0,0 +1,110 @@
+<script>
+import {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
+
+export default {
+ components: {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ iterations: this.config.initialIterations || [],
+ defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS,
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data;
+ },
+ activeIteration() {
+ return this.iterations.find((iteration) => iteration.title === this.currentValue);
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.iterations.length) {
+ this.fetchIterationBySearchTerm(this.currentValue);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchIterationBySearchTerm(searchTerm) {
+ const fetchPromise = this.config.fetchPath
+ ? this.config.fetchIterations(this.config.fetchPath, searchTerm)
+ : this.config.fetchIterations(searchTerm);
+
+ this.loading = true;
+
+ fetchPromise
+ .then((response) => {
+ this.iterations = Array.isArray(response) ? response : response.data;
+ })
+ .catch(() => createFlash({ message: __('There was a problem fetching iterations.') }))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchIterations: debounce(function debouncedSearch({ data }) {
+ this.fetchIterationBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchIterations"
+ >
+ <template #view="{ inputValue }">
+ {{ activeIteration ? activeIteration.title : inputValue }}
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="iteration in defaultIterations"
+ :key="iteration.value"
+ :value="iteration.value"
+ >
+ {{ iteration.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultIterations.length" />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="iteration in iterations"
+ :key="iteration.title"
+ :value="iteration.title"
+ >
+ {{ iteration.title }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
new file mode 100644
index 00000000000..72116f0e991
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
+import { DEFAULT_NONE_ANY } from '../constants';
+
+export default {
+ baseWeights: ['0', '1', '2', '3', '4', '5'],
+ components: {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ weights: this.$options.baseWeights,
+ defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY,
+ };
+ },
+ methods: {
+ updateWeights({ data }) {
+ const weight = parseInt(data, 10);
+ this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="updateWeights"
+ >
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="weight in defaultWeights"
+ :key="weight.value"
+ :value="weight.value"
+ >
+ {{ weight.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultWeights.length" />
+ <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight">
+ {{ weight }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index be0c843ef00..ccdb47e3144 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -146,6 +146,7 @@ export default {
<span v-if="dueDate" class="order-md-1">
<issue-due-date
:date="dueDate"
+ :closed="Boolean(closedAt)"
tooltip-placement="top"
css-class="item-due-date gl-display-flex gl-align-items-center"
/>
diff --git a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue b/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
new file mode 100644
index 00000000000..d68c4399275
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ slotKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ aliveSlotsLookup: {},
+ };
+ },
+ computed: {
+ aliveSlots() {
+ return Object.keys(this.aliveSlotsLookup);
+ },
+ },
+ watch: {
+ slotKey: {
+ handler(val) {
+ if (!val) {
+ return;
+ }
+
+ this.$set(this.aliveSlotsLookup, val, true);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ isCurrentSlot(key) {
+ return key === this.slotKey;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ v-for="slot in aliveSlots"
+ v-show="isCurrentSlot(slot)"
+ :key="slot"
+ class="gl-h-full gl-w-full"
+ >
+ <slot :name="slot"></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 90ac20fe748..d6a20984ad1 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -34,7 +34,7 @@ export default {
boundary="window"
right
menu-class="gl-w-full!"
- data-qa-selector="apply_suggestion_button"
+ data-qa-selector="apply_suggestion_dropdown"
@shown="$refs.commitMessage.$el.focus()"
>
<gl-dropdown-form class="gl-px-4! gl-m-0!">
@@ -45,7 +45,7 @@ export default {
v-model="message"
:placeholder="defaultCommitMessage"
submit-on-enter
- data-qa-selector="commit_message_textbox"
+ data-qa-selector="commit_message_field"
@submit="onApply"
/>
<gl-button
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 01cf0beea3a..d343ba700ab 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -63,6 +63,9 @@ export default {
'\n',
);
},
+ mdCollapsibleSection() {
+ return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n');
+ },
isMac() {
// Accessing properties using ?. to allow tests to use
// this component without setting up window.gl.client.
@@ -245,6 +248,13 @@ export default {
icon="list-task"
/>
<toolbar-button
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ />
+ <toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
index bcd8c02e968..9c954fce322 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue
@@ -70,7 +70,7 @@ export default {
<template>
<div class="md-suggestion">
<suggestion-diff-header
- class="qa-suggestion-diff-header js-suggestion-diff-header"
+ class="js-suggestion-diff-header"
:suggestions-count="suggestionsCount"
:can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled"
:is-applied="suggestion.applied"
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index e2591362611..d05e45e90b3 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -4,6 +4,7 @@ import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import Tracking from '~/tracking';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
@@ -105,7 +106,7 @@ export default {
unique: true,
symbol: '@',
token: AuthorToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
@@ -116,7 +117,7 @@ export default {
unique: true,
symbol: '@',
token: AuthorToken,
- operators: [{ value: '=', description: __('is'), default: 'true' }],
+ operators: OPERATOR_IS_ONLY,
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 4ade75e705e..b9e916bc199 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -32,7 +32,7 @@ export default {
return {
'gl-border-t-transparent': !this.first && !this.selected,
'gl-border-t-gray-100': this.first && !this.selected,
- 'disabled-content': this.disabled,
+ 'gl-opacity-5': this.disabled,
'gl-border-b-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
index dff3a6a8c3f..07272a5b8d6 100644
--- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
@@ -55,13 +55,12 @@ export default {
return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
},
oncallSchedules() {
- let schedules = {};
try {
- schedules = JSON.parse(this.modalData.oncallSchedules);
+ return JSON.parse(this.modalData.oncallSchedules);
} catch (e) {
Sentry.captureException(e);
}
- return schedules;
+ return {};
},
},
mounted() {
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index 795b4f58ac5..1f70644eb2c 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -9,7 +9,9 @@ import {
GlIcon,
GlLoadingIcon,
GlSkeletonLoader,
+ GlResizeObserverDirective,
} from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { isEmpty } from 'lodash';
import { __, s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -33,6 +35,9 @@ export default {
GlSkeletonLoader,
ModalCopyButton,
},
+ directives: {
+ GlResizeObserver: GlResizeObserverDirective,
+ },
props: {
modalId: {
type: String,
@@ -87,6 +92,7 @@ export default {
selectedArchitecture: null,
showAlert: false,
instructions: {},
+ platformsButtonGroupVertical: false,
};
},
computed: {
@@ -127,6 +133,13 @@ export default {
toggleAlert(state) {
this.showAlert = state;
},
+ onPlatformsButtonResize() {
+ if (bp.getBreakpointSize() === 'xs') {
+ this.platformsButtonGroupVertical = true;
+ } else {
+ this.platformsButtonGroupVertical = false;
+ }
+ },
},
i18n: {
installARunner: s__('Runners|Install a runner'),
@@ -159,17 +172,23 @@ export default {
<h5>
{{ __('Environment') }}
</h5>
- <gl-button-group class="gl-mb-3">
- <gl-button
- v-for="platform in platforms"
- :key="platform.name"
- :selected="selectedPlatform && selectedPlatform.name === platform.name"
- data-testid="platform-button"
- @click="selectPlatform(platform)"
+ <div v-gl-resize-observer="onPlatformsButtonResize">
+ <gl-button-group
+ :vertical="platformsButtonGroupVertical"
+ :class="{ 'gl-w-full': platformsButtonGroupVertical }"
+ class="gl-mb-3"
+ data-testid="platform-buttons"
>
- {{ platform.humanReadableName }}
- </gl-button>
- </gl-button-group>
+ <gl-button
+ v-for="platform in platforms"
+ :key="platform.name"
+ :selected="selectedPlatform && selectedPlatform.name === platform.name"
+ @click="selectPlatform(platform)"
+ >
+ {{ platform.humanReadableName }}
+ </gl-button>
+ </gl-button-group>
+ </div>
</template>
<template v-if="hasArchitecureList">
<template v-if="selectedPlatform">
@@ -190,7 +209,7 @@ export default {
{{ architecture.name }}
</gl-dropdown-item>
</gl-dropdown>
- <div class="gl-display-flex gl-align-items-center gl-mb-3">
+ <div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
<h5>{{ $options.i18n.downloadInstallBinary }}</h5>
<gl-button
class="gl-ml-auto"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 9b28ce0d881..94cf1f84ec3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -61,6 +61,7 @@ export default {
</script>
<template>
+ <!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref="dropdownButton"
:class="{ 'js-extra-options': showExtraOptions }"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index e3704198ad0..d80b66fd9be 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import { mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
@@ -18,6 +18,7 @@ export default {
},
computed: {
...mapState(['showDropdownContentsCreateView']),
+ ...mapGetters(['isDropdownVariantSidebar']),
dropdownContentsView() {
if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view';
@@ -25,11 +26,8 @@ export default {
return 'dropdown-contents-labels-view';
},
directionStyle() {
- if (this.renderOnTop) {
- return { bottom: '100%' };
- }
-
- return {};
+ const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
+ return this.renderOnTop ? { bottom } : {};
},
},
};
@@ -37,7 +35,7 @@ export default {
<template>
<div
- class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
+ class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
data-qa-selector="labels_dropdown_content"
:style="directionStyle"
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index 6065b6c160c..86788a84260 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -83,12 +83,13 @@ export default {
const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused');
if (highlightedLabel) {
- const rect = highlightedLabel.getBoundingClientRect();
- if (rect.bottom > this.$refs.labelsListContainer.clientHeight) {
- highlightedLabel.scrollIntoView(false);
- }
- if (rect.top < 0) {
- highlightedLabel.scrollIntoView();
+ const container = this.$refs.labelsListContainer.getBoundingClientRect();
+ const label = highlightedLabel.getBoundingClientRect();
+
+ if (label.bottom > container.bottom) {
+ this.$refs.labelsListContainer.scrollTop += label.bottom - container.bottom;
+ } else if (label.top < container.top) {
+ this.$refs.labelsListContainer.scrollTop -= container.top - label.top;
}
}
},
@@ -177,7 +178,7 @@ export default {
class="labels-fetch-loading gl-align-items-center w-100 h-100"
size="md"
/>
- <ul v-else class="list-unstyled mb-0">
+ <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word">
<label-item
v-for="(label, index) in visibleLabels"
:key="label.id"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
index e431fd000a6..e8fdf4bb0c2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -22,7 +22,7 @@ export default {
const { label, highlight, isLabelSet } = props;
const labelColorBox = h('span', {
- class: 'dropdown-label-box',
+ class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
style: {
backgroundColor: label.color,
},
@@ -33,7 +33,7 @@ export default {
const checkedIcon = h(GlIcon, {
class: {
- 'mr-2 align-self-center': true,
+ 'gl-mr-3 gl-flex-shrink-0': true,
hidden: !isLabelSet,
},
props: {
@@ -43,7 +43,7 @@ export default {
const noIcon = h('span', {
class: {
- 'mr-3 pr-2': true,
+ 'gl-mr-5 gl-pr-3': true,
hidden: isLabelSet,
},
attrs: {
@@ -56,7 +56,7 @@ export default {
const labelLink = h(
GlLink,
{
- class: 'd-flex align-items-baseline text-break-word label-item',
+ class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal',
on: {
click: () => {
listeners.clickLabel(label);
@@ -70,8 +70,8 @@ export default {
'li',
{
class: {
- 'd-block': true,
- 'text-left': true,
+ 'gl-display-block': true,
+ 'gl-text-left': true,
'is-focused': highlight,
},
},
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 f547433f322..a4462895f6a 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
@@ -268,7 +268,7 @@ export default {
this.$emit('toggleCollapse');
},
setContentIsOnViewport(showDropdownContents) {
- if (!this.isDropdownVariantEmbedded || !showDropdownContents) {
+ if (!showDropdownContents) {
this.contentIsOnViewport = true;
return;
@@ -276,8 +276,7 @@ export default {
this.$nextTick(() => {
if (this.$refs.dropdownContents) {
- const offset = { top: 100 };
- this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el, offset);
+ this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
}
});
},
@@ -313,6 +312,7 @@ export default {
<dropdown-contents
v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :render-on-top="!contentIsOnViewport"
/>
</template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
new file mode 100644
index 00000000000..93b9833bb7d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
@@ -0,0 +1,18 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query issueAssignees($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: issue(iid: $iid) {
+ __typename
+ id
+ assignees {
+ nodes {
+ ...User
+ ...UserAvailability
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 3885127fa8e..48787305459 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -13,12 +13,6 @@ query issueParticipants($fullPath: ID!, $iid: String!) {
...UserAvailability
}
}
- assignees {
- nodes {
- ...User
- ...UserAvailability
- }
- }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
new file mode 100644
index 00000000000..a2990d7171b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
@@ -0,0 +1,14 @@
+#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
+
+query timeTrackingReport($id: IssueID!) {
+ issuable: issue(id: $id) {
+ __typename
+ id
+ title
+ timelogs {
+ nodes {
+ ...TimelogFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
new file mode 100644
index 00000000000..53f7381760e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query getMrAssignees($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ issuable: mergeRequest(iid: $iid) {
+ id
+ assignees {
+ nodes {
+ ...User
+ ...UserAvailability
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 63482873b69..6adbd4098f2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -11,12 +11,6 @@ query getMrParticipants($fullPath: ID!, $iid: String!) {
...UserAvailability
}
}
- assignees {
- nodes {
- ...User
- ...UserAvailability
- }
- }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
new file mode 100644
index 00000000000..753f1b345e3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
@@ -0,0 +1,14 @@
+#import "~/graphql_shared/fragments/issuable_timelogs.fragment.graphql"
+
+query timeTrackingReport($id: MergeRequestID!) {
+ issuable: mergeRequest(id: $id) {
+ __typename
+ id
+ title
+ timelogs {
+ nodes {
+ ...TimelogFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
index 3f40c0368d7..24de5ea4fe3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
@@ -13,12 +13,6 @@ mutation issueSetAssignees($iid: String!, $assigneeUsernames: [String!]!, $fullP
...UserAvailability
}
}
- participants {
- nodes {
- ...User
- ...UserAvailability
- }
- }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 4447a87777a..66088b33c99 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -15,7 +15,7 @@ export default {
mixins: [timeagoMixin],
props: {
time: {
- type: String,
+ type: [String, Number],
required: true,
},
tooltipPlacement: {
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 11f484b2cdf..deac24d2270 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -20,7 +20,7 @@ export default {
},
props: {
target: {
- type: HTMLAnchorElement,
+ type: HTMLElement,
required: true,
},
user: {
@@ -79,7 +79,7 @@ export default {
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
- <span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
+ <span ref="bio" class="gl-ml-2 gl-overflow-hidden" v-html="user.bioHtml"></span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
new file mode 100644
index 00000000000..3116d2fbf32
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -0,0 +1,302 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import searchUsers from '~/graphql_shared/queries/users_search.query.graphql';
+import { __ } from '~/locale';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
+import { ASSIGNEES_DEBOUNCE_DELAY, participantsQueries } from '~/sidebar/constants';
+
+export default {
+ i18n: {
+ unassigned: __('Unassigned'),
+ },
+ components: {
+ GlDropdownForm,
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ SidebarParticipant,
+ GlLoadingIcon,
+ },
+ props: {
+ headerText: {
+ type: String,
+ required: true,
+ },
+ text: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Array,
+ required: true,
+ },
+ allowMultipleAssignees: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ currentUser: {
+ type: Object,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ data() {
+ return {
+ search: '',
+ participants: [],
+ searchUsers: [],
+ isSearching: false,
+ };
+ },
+ apollo: {
+ participants: {
+ query() {
+ return participantsQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ iid: this.iid,
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.participants.nodes;
+ },
+ error() {
+ this.$emit('error');
+ },
+ },
+ searchUsers: {
+ // TODO Remove error policy
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ errorPolicy: 'all',
+ query: searchUsers,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.search,
+ first: 20,
+ };
+ },
+ update(data) {
+ // TODO Remove null filter (BE fix required)
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ return data.workspace?.users?.nodes.filter((x) => x).map(({ user }) => user) || [];
+ },
+ debounce: ASSIGNEES_DEBOUNCE_DELAY,
+ error({ graphQLErrors }) {
+ // TODO This error suppression is temporary (BE fix required)
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ const isNullError = ({ message }) => {
+ return message === 'Cannot return null for non-nullable field GroupMember.user';
+ };
+
+ if (graphQLErrors?.length > 0 && graphQLErrors.every(isNullError)) {
+ // only null-related errors exist, suppress them.
+ // eslint-disable-next-line no-console
+ console.error(
+ "Suppressing the error 'Cannot return null for non-nullable field GroupMember.user'. Please see https://gitlab.com/gitlab-org/gitlab/-/issues/329750",
+ );
+ this.isSearching = false;
+ return;
+ }
+
+ this.$emit('error');
+ this.isSearching = false;
+ },
+ result() {
+ this.isSearching = false;
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading;
+ },
+ users() {
+ if (!this.participants) {
+ return [];
+ }
+
+ const filteredParticipants = this.participants.filter(
+ (user) => user.name.includes(this.search) || user.username.includes(this.search),
+ );
+
+ // TODO this de-duplication is temporary (BE fix required)
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/327822
+ const mergedSearchResults = filteredParticipants
+ .concat(this.searchUsers)
+ .reduce(
+ (acc, current) => (acc.some((user) => current.id === user.id) ? acc : [...acc, current]),
+ [],
+ );
+
+ return this.moveCurrentUserToStart(mergedSearchResults);
+ },
+ isSearchEmpty() {
+ return this.search === '';
+ },
+ shouldShowParticipants() {
+ return this.isSearchEmpty || this.isSearching;
+ },
+ isCurrentUserInList() {
+ const isCurrentUser = (user) => user.username === this.currentUser.username;
+ return this.users.some(isCurrentUser);
+ },
+ noUsersFound() {
+ return !this.isSearchEmpty && this.users.length === 0;
+ },
+ showCurrentUser() {
+ return this.currentUser.username && !this.isCurrentUserInList && this.isSearchEmpty;
+ },
+ selectedFiltered() {
+ if (this.shouldShowParticipants) {
+ return this.moveCurrentUserToStart(this.value);
+ }
+
+ const foundUsernames = this.users.map(({ username }) => username);
+ const filtered = this.value.filter(({ username }) => foundUsernames.includes(username));
+ return this.moveCurrentUserToStart(filtered);
+ },
+ selectedUserNames() {
+ return this.value.map(({ username }) => username);
+ },
+ unselectedFiltered() {
+ return this.users?.filter(({ username }) => !this.selectedUserNames.includes(username)) || [];
+ },
+ selectedIsEmpty() {
+ return this.selectedFiltered.length === 0;
+ },
+ },
+ watch: {
+ // We need to add this watcher to track the moment when user is alredy typing
+ // but query is still not started due to debounce
+ search(newVal) {
+ if (newVal) {
+ this.isSearching = true;
+ }
+ },
+ },
+ methods: {
+ selectAssignee(user) {
+ let selected = [...this.value];
+ if (!this.allowMultipleAssignees) {
+ selected = [user];
+ } else {
+ selected.push(user);
+ }
+ this.$emit('input', selected);
+ },
+ unselect(name) {
+ const selected = this.value.filter((user) => user.username !== name);
+ this.$emit('input', selected);
+ },
+ focusSearch() {
+ this.$refs.search.focusInput();
+ },
+ showDivider(list) {
+ return list.length > 0 && this.isSearchEmpty;
+ },
+ moveCurrentUserToStart(users) {
+ if (!users) {
+ return [];
+ }
+ const usersCopy = [...users];
+ const currentUser = usersCopy.find((user) => user.username === this.currentUser.username);
+
+ if (currentUser) {
+ const index = usersCopy.indexOf(currentUser);
+ usersCopy.splice(0, 0, usersCopy.splice(index, 1)[0]);
+ }
+
+ return usersCopy;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown class="show" :text="text" @toggle="$emit('toggle')">
+ <template #header>
+ <p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
+ <gl-dropdown-divider />
+ <gl-search-box-by-type ref="search" v-model.trim="search" class="js-dropdown-input-field" />
+ </template>
+ <gl-dropdown-form class="gl-relative gl-min-h-7">
+ <gl-loading-icon
+ v-if="isLoading"
+ data-testid="loading-participants"
+ size="md"
+ class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
+ />
+ <template v-else>
+ <template v-if="shouldShowParticipants">
+ <gl-dropdown-item
+ v-if="isSearchEmpty"
+ :is-checked="selectedIsEmpty"
+ :is-check-centered="true"
+ data-testid="unassign"
+ @click="$emit('input', [])"
+ >
+ <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{
+ $options.i18n.unassigned
+ }}</span></gl-dropdown-item
+ >
+ </template>
+ <gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
+ <gl-dropdown-item
+ v-for="item in selectedFiltered"
+ :key="item.id"
+ is-checked
+ is-check-centered
+ data-testid="selected-participant"
+ @click.stop="unselect(item.username)"
+ >
+ <sidebar-participant :user="item" />
+ </gl-dropdown-item>
+ <template v-if="showCurrentUser">
+ <gl-dropdown-divider />
+ <gl-dropdown-item data-testid="current-user" @click.stop="selectAssignee(currentUser)">
+ <sidebar-participant :user="currentUser" class="gl-pl-6!" />
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unselectedFiltered"
+ :key="unselectedUser.id"
+ data-testid="unselected-participant"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <sidebar-participant :user="unselectedUser" class="gl-pl-6!" />
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="noUsersFound" data-testid="empty-results" class="gl-pl-6!">
+ {{ __('No matching results') }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown-form>
+ <template #footer>
+ <slot name="footer"></slot>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
new file mode 100644
index 00000000000..eff39e2fb89
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
@@ -0,0 +1,21 @@
+<script>
+export default {
+ provide() {
+ return {
+ // We can't use this.vuexModule due to bug in vue-apollo when
+ // provide is called in beforeCreate
+ // See https://github.com/vuejs/vue-apollo/pull/1153 for details
+ vuexModule: this.$options.propsData.vuexModule,
+ };
+ },
+ props: {
+ vuexModule: {
+ type: String,
+ required: true,
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index 176954891e9..692f2769b88 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -33,6 +33,10 @@ const focusFirstInvalidInput = (e) => {
}
};
+const getInputElement = (el) => {
+ return el.querySelector('input') || el;
+};
+
const isEveryFieldValid = (form) => Object.values(form.fields).every(({ state }) => state === true);
const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = false }) => {
@@ -91,8 +95,9 @@ export default function initValidation(customFeedbackMap = {}) {
const elDataMap = new WeakMap();
return {
- inserted(el, binding, { context }) {
+ inserted(element, binding, { context }) {
const { arg: showGlobalValidation } = binding;
+ const el = getInputElement(element);
const { form: formEl } = el;
const validate = createValidator(context, feedbackMap);
@@ -121,7 +126,8 @@ export default function initValidation(customFeedbackMap = {}) {
validate({ el, reportInvalidInput: showGlobalValidation });
},
- update(el, binding) {
+ update(element, binding) {
+ const el = getInputElement(element);
const { arg: showGlobalValidation } = binding;
const { validate, isTouched, isBlurred } = elDataMap.get(el);
const showValidationFeedback = showGlobalValidation || (isTouched && isBlurred);
@@ -130,3 +136,59 @@ export default function initValidation(customFeedbackMap = {}) {
},
};
}
+
+/**
+ * This is a helper that initialize the form fields structure to be used in initForm
+ * @param {*} fieldValues
+ * @returns formObject
+ */
+const initFormField = ({ value, required = true, skipValidation = false }) => ({
+ value,
+ required,
+ state: skipValidation ? true : null,
+ feedback: null,
+});
+
+/**
+ * This is a helper that initialize the form structure that is compliant to be used with the validation directive
+ *
+ * @example
+ * const form initForm = initForm({
+ * fields: {
+ * name: {
+ * value: 'lorem'
+ * },
+ * description: {
+ * value: 'ipsum',
+ * required: false,
+ * skipValidation: true
+ * }
+ * }
+ * })
+ *
+ * @example
+ * const form initForm = initForm({
+ * state: true, // to override
+ * foo: { // something custom
+ * bar: 'lorem'
+ * },
+ * fields: {...}
+ * })
+ *
+ * @param {*} formObject
+ * @returns form
+ */
+export const initForm = ({ fields = {}, ...rest } = {}) => {
+ const initFields = Object.fromEntries(
+ Object.entries(fields).map(([fieldName, fieldValues]) => {
+ return [fieldName, initFormField(fieldValues)];
+ }),
+ );
+
+ return {
+ state: false,
+ showValidation: false,
+ ...rest,
+ fields: initFields,
+ };
+};
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index af14c6d9486..45452f2ea35 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -14,5 +14,25 @@ export default {
tooltipTitle(time) {
return formatDate(time);
},
+
+ durationTimeFormatted(duration) {
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) {
+ hh = `0${hh}`;
+ }
+ if (mm < 10) {
+ mm = `0${mm}`;
+ }
+ if (ss < 10) {
+ ss = `0${ss}`;
+ }
+
+ return `${hh}:${mm}:${ss}`;
+ },
},
};
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/legacy_container.vue b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
index d2fc2c66924..d2fc2c66924 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/legacy_container.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
new file mode 100644
index 00000000000..e9983af5401
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import Vue from 'vue';
+import Tracking from '~/tracking';
+
+export default {
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ panels: {
+ type: Array,
+ required: true,
+ },
+ experiment: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ created() {
+ const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment });
+ const trackingInstance = new Vue({
+ ...trackingMixin,
+ render() {
+ return null;
+ },
+ });
+ this.track = trackingInstance.track;
+ },
+};
+</script>
+<template>
+ <div class="container">
+ <h2 class="gl-my-7 gl-font-size-h1 gl-text-center">
+ {{ title }}
+ </h2>
+ <div>
+ <div
+ v-for="panel in panels"
+ :key="panel.name"
+ class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5"
+ >
+ <a
+ :href="`#${panel.name}`"
+ :data-qa-selector="`${panel.name}_link`"
+ class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!"
+ @click="track('click_tab', { label: panel.name })"
+ >
+ <div
+ v-safe-html="panel.illustration"
+ class="new-namespace-panel-illustration gl-text-white gl-display-flex gl-flex-shrink-0 gl-justify-content-center"
+ ></div>
+ <div class="gl-pl-4">
+ <h3 class="gl-font-size-h2 gl-reset-color">
+ {{ panel.title }}
+ </h3>
+ <p class="gl-text-gray-900">
+ {{ panel.description }}
+ </p>
+ </div>
+ </a>
+ </div>
+ </div>
+ <slot name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
new file mode 100644
index 00000000000..54313297b14
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -0,0 +1,135 @@
+<script>
+import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+
+import LegacyContainer from './components/legacy_container.vue';
+import WelcomePage from './components/welcome.vue';
+
+export default {
+ components: {
+ GlBreadcrumb,
+ GlIcon,
+ WelcomePage,
+ LegacyContainer,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ initialBreadcrumb: {
+ type: String,
+ required: true,
+ },
+ panels: {
+ type: Array,
+ required: true,
+ },
+ jumpToLastPersistedPanel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ persistenceKey: {
+ type: String,
+ required: true,
+ },
+ experiment: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+
+ data() {
+ return {
+ activePanelName: null,
+ };
+ },
+
+ computed: {
+ activePanel() {
+ return this.panels.find((p) => p.name === this.activePanelName);
+ },
+
+ details() {
+ return this.activePanel.details || this.activePanel.description;
+ },
+
+ hasTextDetails() {
+ return typeof this.details === 'string';
+ },
+
+ breadcrumbs() {
+ if (!this.activePanel) {
+ return null;
+ }
+
+ return [
+ { text: this.initialBreadcrumb, href: '#' },
+ { text: this.activePanel.title, href: `#${this.activePanel.name}` },
+ ];
+ },
+ },
+
+ created() {
+ this.handleLocationHashChange();
+
+ if (this.jumpToLastPersistedPanel) {
+ this.activePanelName = localStorage.getItem(this.persistenceKey) || this.panels[0].name;
+ }
+
+ window.addEventListener('hashchange', () => {
+ this.handleLocationHashChange();
+ this.$emit('panel-change');
+ });
+
+ this.$root.$on('clicked::link', (e) => {
+ window.location = e.target.href;
+ });
+ },
+
+ methods: {
+ handleLocationHashChange() {
+ this.activePanelName = window.location.hash.substring(1) || null;
+ if (this.activePanelName) {
+ localStorage.setItem(this.persistenceKey, this.activePanelName);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <welcome-page
+ v-if="activePanelName === null"
+ :panels="panels"
+ :title="title"
+ :experiment="experiment"
+ >
+ <template #footer>
+ <slot name="welcome-footer"> </slot>
+ </template>
+ </welcome-page>
+ <div v-else class="row">
+ <div class="col-lg-3">
+ <div v-safe-html="activePanel.illustration" class="gl-text-white"></div>
+ <h4>{{ activePanel.title }}</h4>
+
+ <p v-if="hasTextDetails">{{ details }}</p>
+ <component :is="details" v-else />
+
+ <slot name="extra-description"></slot>
+ </div>
+ <div class="col-lg-9">
+ <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs">
+ <template #separator>
+ <gl-icon name="chevron-right" :size="8" />
+ </template>
+ </gl-breadcrumb>
+ <legacy-container :key="activePanel.name" class="gl-mt-3" :selector="activePanel.selector" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
new file mode 100644
index 00000000000..12e5f634a08
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { sprintf, s__ } from '~/locale';
+import apolloProvider from '../provider';
+
+export default {
+ apolloProvider,
+ components: {
+ GlButton,
+ },
+ inject: ['projectPath'],
+ props: {
+ feature: {
+ type: Object,
+ required: true,
+ },
+ variant: {
+ type: String,
+ required: false,
+ default: 'success',
+ },
+ category: {
+ type: String,
+ required: false,
+ default: 'secondary',
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ computed: {
+ featureSettings() {
+ return featureToMutationMap[this.feature.type];
+ },
+ },
+ methods: {
+ async mutate() {
+ this.isLoading = true;
+ try {
+ const mutation = this.featureSettings;
+ const { data } = await this.$apollo.mutate(mutation.getMutationPayload(this.projectPath));
+ const { errors, successPath } = data[mutation.mutationId];
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+
+ if (!successPath) {
+ throw new Error(
+ sprintf(this.$options.i18n.noSuccessPathError, { featureName: this.feature.name }),
+ );
+ }
+
+ redirectTo(successPath);
+ } catch (e) {
+ this.$emit('error', e.message);
+ this.isLoading = false;
+ }
+ },
+ },
+ i18n: {
+ buttonLabel: s__('SecurityConfiguration|Configure via Merge Request'),
+ noSuccessPathError: s__(
+ 'SecurityConfiguration|%{featureName} merge request creation mutation failed',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ v-if="!feature.configured"
+ :loading="isLoading"
+ :variant="variant"
+ :category="category"
+ @click="mutate"
+ >{{ $options.i18n.buttonLabel }}</gl-button
+ >
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/provider.js b/app/assets/javascripts/vue_shared/security_configuration/provider.js
new file mode 100644
index 00000000000..ef96b443da8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_configuration/provider.js
@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export default new VueApollo({
+ defaultClient: createDefaultClient(),
+});
diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql
index 4ce13827da2..4ce13827da2 100644
--- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql
diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql
new file mode 100644
index 00000000000..c7e9fa16418
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql
@@ -0,0 +1,18 @@
+query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
+ project(fullPath: $projectPath) {
+ pipeline(iid: $iid) {
+ id
+ jobs(securityReportTypes: $reportTypes) {
+ nodes {
+ name
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ }
+}
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 1151cffa76f..b7f283b8fd9 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
@@ -13,10 +13,10 @@ import {
REPORT_TYPE_SECRET_DETECTION,
reportTypeToSecurityReportTypeEnum,
} from './constants';
-import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
+import securityReportMergeRequestDownloadPathsQuery from './queries/security_report_merge_request_download_paths.query.graphql';
import store from './store';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
-import { extractSecurityReportArtifacts } from './utils';
+import { extractSecurityReportArtifactsFromMergeRequest } from './utils';
export default {
store,
@@ -86,7 +86,7 @@ export default {
},
apollo: {
reportArtifacts: {
- query: securityReportDownloadPathsQuery,
+ query: securityReportMergeRequestDownloadPathsQuery,
variables() {
return {
projectPath: this.targetProjectFullPath,
@@ -97,7 +97,7 @@ export default {
};
},
update(data) {
- return extractSecurityReportArtifacts(this.$options.reportTypes, data);
+ return extractSecurityReportArtifactsFromMergeRequest(this.$options.reportTypes, data);
},
error(error) {
this.showError(error);
diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js
index ad819bf7081..c3f24a7e52f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/utils.js
@@ -14,9 +14,7 @@ const addReportTypeIfExists = (acc, reportTypes, reportType, getName, downloadPa
}
};
-export const extractSecurityReportArtifacts = (reportTypes, data) => {
- const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
-
+const extractSecurityReportArtifacts = (reportTypes, jobs) => {
return jobs.reduce((acc, job) => {
const artifacts = job.artifacts?.nodes ?? [];
@@ -41,3 +39,13 @@ export const extractSecurityReportArtifacts = (reportTypes, data) => {
return acc;
}, []);
};
+
+export const extractSecurityReportArtifactsFromPipeline = (reportTypes, data) => {
+ const jobs = data.project?.pipeline?.jobs?.nodes ?? [];
+ return extractSecurityReportArtifacts(reportTypes, jobs);
+};
+
+export const extractSecurityReportArtifactsFromMergeRequest = (reportTypes, data) => {
+ const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
+ return extractSecurityReportArtifacts(reportTypes, jobs);
+};
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index 4a387edbe3f..4ee586527b5 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -30,7 +30,7 @@ export default {
},
mounted() {
this.openDrawer(this.versionDigest);
- this.fetchItems();
+ this.fetchFreshItems();
const body = document.querySelector('body');
const namespaceId = body.getAttribute('data-namespace-id');
@@ -42,13 +42,18 @@ export default {
bottomReached() {
const page = this.pageInfo.nextPage;
if (page) {
- this.fetchItems({ page });
+ this.fetchFreshItems(page);
}
},
handleResize() {
const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height);
},
+ fetchFreshItems(page) {
+ const { versionDigest } = this;
+
+ this.fetchItems({ page, versionDigest });
+ },
},
};
</script>
@@ -58,7 +63,7 @@ export default {
<gl-drawer
ref="drawer"
v-gl-resize-observer="handleResize"
- class="whats-new-drawer"
+ class="whats-new-drawer gl-reset-line-height"
:z-index="700"
:open="open"
@close="closeDrawer"
@@ -83,6 +88,6 @@ export default {
<skeleton-loader />
</div>
</gl-drawer>
- <div v-if="open" class="whats-new-modal-backdrop modal-backdrop"></div>
+ <div v-if="open" class="whats-new-modal-backdrop modal-backdrop" @click="closeDrawer"></div>
</div>
</template>
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue
index f6f7618b0d8..5444e77a4d2 100644
--- a/app/assets/javascripts/whats_new/components/feature.vue
+++ b/app/assets/javascripts/whats_new/components/feature.vue
@@ -1,11 +1,13 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
+import { dateInWords, isValidDate } from '~/lib/utils/datetime_utility';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
+ GlButton,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@@ -16,52 +18,68 @@ export default {
required: true,
},
},
+ computed: {
+ releaseDate() {
+ const { published_at } = this.feature;
+ const date = new Date(published_at);
+
+ if (!isValidDate(date) || date.getTime() === 0) {
+ return '';
+ }
+
+ return dateInWords(date);
+ },
+ },
};
</script>
<template>
- <div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
+ <div class="gl-py-6 gl-px-6 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
+ <gl-link
+ :href="feature.url"
+ target="_blank"
+ class="gl-display-block"
+ data-track-event="click_whats_new_item"
+ :data-track-label="feature.title"
+ :data-track-property="feature.url"
+ >
+ <div
+ class="whats-new-item-image gl-bg-size-cover"
+ :style="`background-image: url(${feature.image_url});`"
+ >
+ <span class="gl-sr-only">{{ feature.title }}</span>
+ </div>
+ </gl-link>
<gl-link
:href="feature.url"
target="_blank"
- class="whats-new-item-title-link"
+ class="whats-new-item-title-link gl-display-block gl-mt-4 gl-mb-1"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
- <h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
+ <h5 class="gl-font-lg gl-my-0" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
+ <div v-if="releaseDate" class="gl-mb-3" data-testid="release-date">{{ releaseDate }}</div>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
- size="sm"
+ size="md"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
- <gl-link
- :href="feature.url"
- target="_blank"
- data-track-event="click_whats_new_item"
- :data-track-label="feature.title"
- :data-track-property="feature.url"
- >
- <img
- :alt="feature.title"
- :src="feature.image_url"
- class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
- />
- </gl-link>
- <div v-safe-html="feature.body" class="gl-pt-3"></div>
- <gl-link
+ <div v-safe-html="feature.body" class="gl-pt-3 gl-line-height-20"></div>
+ <gl-button
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
- >{{ __('Learn more') }}</gl-link
>
+ {{ __('Learn more') }} <gl-icon name="arrow-right" />
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
index 1dc92ea2606..f209f145884 100644
--- a/app/assets/javascripts/whats_new/store/actions.js
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -14,17 +14,19 @@ export default {
localStorage.setItem(STORAGE_KEY, versionDigest);
}
},
- fetchItems({ commit, state }, { page } = { page: null }) {
+ fetchItems({ commit, state }, { page, versionDigest } = { page: null, versionDigest: null }) {
if (state.fetching) {
return false;
}
commit(types.SET_FETCHING, true);
+ const v = versionDigest;
return axios
.get('/-/whats_new', {
params: {
page,
+ v,
},
})
.then(({ data, headers }) => {
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index a5cfc8d12b0..c4f292dd05d 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -105,10 +105,6 @@ hr {
}
}
-kbd {
- display: inline-block;
-}
-
code {
padding: 2px 4px;
color: $code-color;
diff --git a/app/assets/stylesheets/components/feature_highlight.scss b/app/assets/stylesheets/components/feature_highlight.scss
index 08706951967..54123e74675 100644
--- a/app/assets/stylesheets/components/feature_highlight.scss
+++ b/app/assets/stylesheets/components/feature_highlight.scss
@@ -7,3 +7,25 @@
padding: 0.25rem;
}
}
+
+.gl-order-1 {
+ order: 1;
+}
+
+.gl-sm-order-init {
+ @media (min-width: $breakpoint-sm) {
+ order: initial;
+ }
+}
+
+.gl-xs-ml-3 {
+ @media (max-width: $breakpoint-sm) {
+ @include gl-ml-3;
+ }
+}
+
+.gl-sm-mr-3 {
+ @media (min-width: $breakpoint-sm) {
+ @include gl-mr-3;
+ }
+}
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
index 3e9060e869b..7af97505749 100644
--- a/app/assets/stylesheets/components/whats_new.scss
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -2,6 +2,7 @@
margin-top: $header-height;
@include gl-shadow-none;
overflow-y: hidden;
+ width: 500px;
.gl-infinite-scroll-legend {
@include gl-display-none;
@@ -54,18 +55,9 @@
.whats-new-item-image {
border-color: $gray-50;
+ height: 250px;
}
.whats-new-modal-backdrop {
z-index: 699;
}
-
-.whats-new-notification-count {
- @include gl-bg-gray-900;
- @include gl-font-sm;
- @include gl-line-height-normal;
- @include gl-text-white;
- @include gl-vertical-align-top;
- border-radius: 20px;
- padding: 3px 10px;
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 1fe94a796f5..cde5ad24fa5 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -22,6 +22,7 @@
@import 'framework/flash';
@import 'framework/forms';
@import 'framework/gfm';
+@import 'framework/kbd';
@import 'framework/header';
@import 'framework/highlight';
@import 'framework/issue_box';
@@ -45,7 +46,6 @@
@import 'framework/toggle';
@import 'framework/typography';
@import 'framework/zen';
-@import 'framework/blank';
@import 'framework/wells';
@import 'framework/page_header';
@import 'framework/page_title';
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
deleted file mode 100644
index 0ada5fabde9..00000000000
--- a/app/assets/stylesheets/framework/blank.scss
+++ /dev/null
@@ -1,136 +0,0 @@
-.blank-state-parent-container {
- .section-container {
- padding: 10px;
- }
-
- .section-body {
- width: 100%;
- height: 100%;
- padding-bottom: 25px;
- border-radius: $border-radius-default;
- }
-}
-
-.blank-state-row {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
-}
-
-.blank-state-welcome {
- text-align: center;
- padding: $gl-padding 0 ($gl-padding * 2);
-
- .blank-state-welcome-title {
- font-size: 24px;
- }
-
- .blank-state-text {
- margin-bottom: 0;
- }
-}
-
-.blank-state-link {
- color: $gl-text-color;
- margin-bottom: 15px;
-
- &:hover {
- background-color: $gray-light;
- text-decoration: none;
- color: $gl-text-color;
- }
-}
-
-.blank-state-center {
- padding-top: 20px;
- padding-bottom: 20px;
- text-align: center;
-}
-
-.blank-state {
- display: flex;
- align-items: center;
- padding: 20px 50px;
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- min-height: 240px;
- margin-bottom: $gl-padding;
- width: calc(50% - #{$gl-padding-8});
-
- @include media-breakpoint-down(sm) {
- width: 100%;
- flex-direction: column;
- justify-content: center;
- padding: 50px 20px;
-
- .column-small & {
- width: 100%;
- }
-
- }
-}
-
-.blank-state,
-.blank-state-center {
- .blank-state-icon {
- svg {
- display: block;
- margin: auto;
- }
- }
-
- .blank-state-title {
- margin-top: 0;
- font-size: 18px;
- }
-
- .blank-state-body {
- @include media-breakpoint-down(sm) {
- text-align: center;
- margin-top: 20px;
- }
-
- @include media-breakpoint-up(sm) {
- padding-left: 20px;
- }
- }
-}
-
-@include media-breakpoint-up(lg) {
- .column-large {
- flex: 2;
- }
-
- .column-small {
- flex: 1;
- margin-bottom: 15px;
-
- .blank-state {
- max-width: 400px;
- flex-wrap: wrap;
- margin-left: 15px;
- }
-
- .blank-state-icon {
- margin-bottom: 30px;
- }
- }
-}
-
-.experiment-new-project-page-blank-state {
- @include media-breakpoint-down(md) {
- flex-direction: column;
- justify-content: center;
- text-align: center;
- }
-
- .blank-state-icon {
- min-width: 215px;
- }
-}
-
-$experiment-new-project-indigo-700: #41419f;
-
-.experiment-new-project-page-blank-state-title {
- color: $experiment-new-project-indigo-700;
-}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index d1fa1187703..603d28a8395 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -337,7 +337,7 @@
.btn-loading {
&:not(.disabled) {
- .spinner {
+ .gl-spinner {
display: none;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 652ffd79ab3..a7ce19ffc69 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -93,6 +93,9 @@
.tab-content {
overflow: visible;
+ @include media-breakpoint-down(sm) {
+ isolation: isolate;
+ }
}
pre {
@@ -266,12 +269,6 @@ img.emoji {
height: 220px;
}
-.description-block {
- @extend .light-well;
- @extend .light;
- margin-bottom: 10px;
-}
-
.footer-links {
margin-bottom: 20px;
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index c5467c304ec..14d1a0663d0 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -429,30 +429,6 @@
display: none;
}
-.toggle-mobile-nav {
- display: none;
- background-color: transparent;
- border: 0;
- padding: 6px 16px;
- margin: 0 0 0 -15px;
- height: 46px;
- color: $gl-text-color;
-
- @include media-breakpoint-down(sm) {
- display: flex;
- align-items: center;
-
- i {
- font-size: 18px;
- }
-
- + .breadcrumbs-links {
- padding-left: $gl-padding;
- border-left: 1px solid $gl-text-color-quaternary;
- }
- }
-}
-
@include media-breakpoint-down(sm) {
.close-nav-button {
display: flex;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index bc7a31c112f..a07e0b48cff 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -670,10 +670,6 @@ table.code {
float: right;
}
-.files-changed {
- border-bottom: 0;
-}
-
.merge-request-details .file-content.image_file img {
max-height: 50vh;
}
@@ -733,7 +729,7 @@ table.code {
}
.files {
- .diff-file:last-child {
+ .diff-file:not(.is-virtual-scrolling):last-child {
margin-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ff42cd836da..894eddbe1a7 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -147,7 +147,7 @@
position: absolute;
}
- .spinner {
+ .gl-spinner {
position: absolute;
top: 9px;
right: 8px;
@@ -266,15 +266,6 @@
}
}
- .shortcut-mappings {
- display: none;
- }
-
- &.shortcuts .shortcut-mappings {
- display: inline-block;
- margin-right: 5px;
- }
-
ul {
margin: 0;
padding: 0;
@@ -848,12 +839,56 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.frequent-items-dropdown-container {
display: flex;
flex-direction: row;
- width: 500px;
- height: 354px;
+ height: $grid-size * 40;
+
+ &.with-deprecated-styles {
+ width: 500px;
+ height: 354px;
+
+ .section-header,
+ .frequent-items-list-container li.section-empty {
+ padding: 0 $gl-padding;
+ }
+
+ .search-input-container {
+ position: relative;
+ padding: 4px $gl-padding;
+
+ .search-icon {
+ position: absolute;
+ top: 13px;
+ right: 25px;
+ color: $gray-300;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ flex-direction: column;
+ width: 100%;
+ height: auto;
+ flex: 1;
+
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
+ width: 100%;
+ }
+
+ .frequent-items-dropdown-sidebar {
+ border-bottom: 1px solid $border-color;
+ border-right: 0;
+ }
+ }
+
+ .frequent-items-list-container {
+ width: auto;
+ height: auto;
+ padding-bottom: 0;
+ }
+ }
.frequent-items-dropdown-sidebar,
.frequent-items-dropdown-content {
- padding: 8px 0;
+ @include gl-pt-3;
}
.loading-animation {
@@ -870,32 +905,13 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
width: 70%;
}
- @include media-breakpoint-down(xs) {
- flex-direction: column;
- width: 100%;
- height: auto;
- flex: 1;
-
- .frequent-items-dropdown-sidebar,
- .frequent-items-dropdown-content {
- width: 100%;
- }
-
- .frequent-items-dropdown-sidebar {
- border-bottom: 1px solid $border-color;
- border-right: 0;
- }
- }
-
.section-header,
.frequent-items-list-container li.section-empty {
- padding: 0 $gl-padding;
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
.frequent-items-list-container {
- height: 304px;
padding: 8px 0;
overflow-y: auto;
@@ -908,36 +924,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
- .search-input-container {
- position: relative;
- padding: 4px $gl-padding;
-
- .search-icon {
- position: absolute;
- top: 13px;
- right: 25px;
- color: $gray-300;
- }
- }
-
.section-header {
font-weight: 700;
margin-top: 8px;
}
-
- @include media-breakpoint-down(xs) {
- .frequent-items-list-container {
- width: auto;
- height: auto;
- padding-bottom: 0;
- }
- }
}
.frequent-items-list-item-container {
.frequent-items-item-avatar-container,
.frequent-items-item-metadata-container {
- float: left;
+ flex-shrink: 0;
}
.frequent-items-item-metadata-container {
diff --git a/app/assets/stylesheets/framework/editor-lite.scss b/app/assets/stylesheets/framework/editor-lite.scss
index 78995c6e4f5..05b53e0c3d8 100644
--- a/app/assets/stylesheets/framework/editor-lite.scss
+++ b/app/assets/stylesheets/framework/editor-lite.scss
@@ -11,7 +11,7 @@
&::before {
content: '';
- @include spinner(32px, 3px);
+ @include spinner-deprecated(32px, 3px);
@include gl-absolute;
@include gl-z-index-1;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 432be7d0b3f..7566a533911 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,3 +1,5 @@
+$top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important;
+
.navbar-gitlab {
padding: 0 16px;
z-index: $header-zindex;
@@ -254,6 +256,7 @@
}
}
+ .top-nav-toggle,
> button {
background: transparent;
border: 0;
@@ -605,3 +608,60 @@
@include media-breakpoint-down(xs) { margin-right: 3px; }
}
+
+.toggle-mobile-nav {
+ display: none;
+ background-color: transparent;
+ border: 0;
+ padding: 6px 16px;
+ margin: 0 0 0 -15px;
+ height: 46px;
+ color: $gl-text-color;
+
+ @include media-breakpoint-down(sm) {
+ display: flex;
+ align-items: center;
+
+ i {
+ font-size: 18px;
+ }
+
+ + .breadcrumbs-links {
+ padding-left: $gl-padding;
+ border-left: 1px solid $gl-text-color-quaternary;
+ }
+ }
+}
+
+.top-nav-container-view {
+ .gl-new-dropdown & .gl-search-box-by-type {
+ @include gl-m-0;
+ }
+
+ .frequent-items-list-item-container > a:hover {
+ background-color: $top-nav-hover-bg;
+ }
+}
+
+.top-nav-toggle {
+ .dropdown-icon {
+ @include gl-mr-3;
+ }
+
+ .dropdown-chevron {
+ top: 0;
+ }
+}
+
+.top-nav-menu-item {
+ color: var(--indigo-900, $theme-indigo-900) !important;
+
+ &.active,
+ &:hover {
+ background-color: $top-nav-hover-bg;
+ }
+
+ .gl-icon {
+ color: inherit !important;
+ }
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 4d5032ac674..8baf70da0c6 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -5,16 +5,7 @@
*/
.status-box {
-
- /* Extra small devices (phones, less than 768px) */
- /* No media query since this is the default in Bootstrap */
- padding: 5px 11px;
- margin-top: 4px;
- /* Small devices (tablets, 768px and up) */
- @include media-breakpoint-up(sm) {
- padding: 0 $gl-btn-padding;
- margin-top: 5px;
- }
+ padding: 0 $gl-btn-padding;
border-radius: $border-radius-default;
display: block;
diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss
new file mode 100644
index 00000000000..05991bc16fd
--- /dev/null
+++ b/app/assets/stylesheets/framework/kbd.scss
@@ -0,0 +1,15 @@
+kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font-size: $gl-font-size-monospace-sm;
+ line-height: 10px;
+ color: var(--gray-700, $gray-700);
+ vertical-align: middle;
+ background-color: var(--gray-10, $gray-10);
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--gray-100, $gray-100) var(--gray-100, $gray-100) var(--gray-200, $gray-200);
+ border-image: none;
+ border-radius: 3px;
+ box-shadow: 0 -1px 0 var(--gray-200, $gray-200) inset;
+}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index df2ba718c72..a3e8b2c245c 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -146,7 +146,7 @@ ul.content-list {
> .btn,
> .btn-group,
> .dropdown.inline {
- margin-right: $gl-padding-top;
+ margin-right: $grid-size;
display: inline-block;
margin-top: 3px;
margin-bottom: 4px;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 27b7cac2df5..f904ef11f5b 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -182,6 +182,11 @@
width: 100%;
}
+ /* This resets the width of the control so that the search button doesn't wrap */
+ .gl-search-box-by-click .form-control {
+ width: 1%;
+ }
+
.dropdown-menu-toggle {
margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index c8eadce5c51..afd2e7ff757 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -1,16 +1,20 @@
-@mixin spinner-color($color) {
+/**
+* Do not use these spinner mixins. Rely on GitLab UI
+* GlLoadingIcon component instead.
+*/
+@mixin spinner-color-deprecated($color) {
border-color: rgba($color, 0.25);
border-top-color: $color;
}
-@mixin spinner-size($size, $border-width) {
+@mixin spinner-size-deprecated($size, $border-width) {
width: $size;
height: $size;
border-width: $border-width;
@include webkit-prefix(transform-origin, 50% 50% calc((#{$size} / 2) + #{$border-width}));
}
-@keyframes spinner-rotate {
+@keyframes spinner-rotate-deprecated {
0% {
transform: rotate(0);
}
@@ -20,47 +24,16 @@
}
}
-@mixin spinner($size: 16px, $border-width: 2px, $color: $gray-700) {
+@mixin spinner-deprecated($size: 16px, $border-width: 2px, $color: $gray-700) {
border-radius: 50%;
position: relative;
margin: 0 auto;
- animation-name: spinner-rotate;
+ animation-name: spinner-rotate-deprecated;
animation-duration: 0.6s;
animation-timing-function: linear;
animation-iteration-count: infinite;
border-style: solid;
display: inline-flex;
- @include spinner-size($size, $border-width);
- @include spinner-color($color);
-}
-
-.spinner {
- @include spinner;
-
- &.spinner-md {
- @include spinner-size(32px, 3px);
- }
-
- &.spinner-lg {
- @include spinner-size(64px, 4px);
- }
-
- &.spinner-dark {
- @include spinner-color($gray-700);
- }
-
- &.spinner-light {
- @include spinner-color($white);
- }
-}
-
-.btn {
- .spinner,
- .gl-spinner {
- vertical-align: text-bottom;
- }
-}
-
-.spin {
- animation: spinner-rotate 2s infinite linear;
+ @include spinner-size-deprecated($size, $border-width);
+ @include spinner-color-deprecated($color);
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 1504f3ee50f..9b38e842635 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -50,6 +50,12 @@
img.avatar {
margin-right: $gl-padding;
+
+ @include media-breakpoint-down(sm) {
+ width: $gl-spacing-scale-6;
+ height: $gl-spacing-scale-6;
+ margin-right: $gl-padding-8;
+ }
}
.controls {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 648ae29e212..603b05efe10 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -81,22 +81,6 @@
word-break: keep-all;
}
- kbd {
- display: inline-block;
- padding: 3px 5px;
- font-size: 11px;
- line-height: 10px;
- color: $gray-700;
- vertical-align: middle;
- background-color: $gray-10;
- border-width: 1px;
- border-style: solid;
- border-color: $gray-100 $gray-100 $gray-200;
- border-image: none;
- border-radius: 3px;
- box-shadow: 0 -1px 0 $gray-200 inset;
- }
-
h1 {
font-size: 1.75em;
font-weight: $gl-font-weight-bold;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 18aa0d3013d..bfb21d7112b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -283,6 +283,8 @@ $indigo-700: #4b4ba3;
$indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
+// To do this variant right for darkmode, we need to create a variable for it.
+$indigo-900-alpha-008: rgba($indigo-900, 0.08);
$theme-blue-50: #f4f8fc;
$theme-blue-100: #e6edf5;
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 0d6f360112b..2f8602a212d 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -147,11 +147,11 @@
display: block;
&:hover {
- box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
+ box-shadow: inset 0 0 0 2px var(--gray-400, $gray-400);
background-color: var(--gray-50, $gray-50);
}
- .spinner,
+ .gl-spinner,
svg {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
@@ -176,12 +176,6 @@
li {
position: relative;
- // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
- &:hover > .mini-pipeline-graph-dropdown-item,
- &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
- @extend .mini-pipeline-graph-dropdown-item:hover;
- }
-
// link to the build
.mini-pipeline-graph-dropdown-item {
align-items: center;
@@ -216,13 +210,16 @@
display: block;
}
}
+ }
- &:hover,
- &:focus {
- outline: none;
- text-decoration: none;
- background-color: var(--gray-100, $gray-50);
- }
+ // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
+ &:hover > .mini-pipeline-graph-dropdown-item,
+ &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item,
+ .mini-pipeline-graph-dropdown-item:hover,
+ .mini-pipeline-graph-dropdown-item:focus {
+ outline: none;
+ text-decoration: none;
+ background-color: var(--gray-100, $gray-50);
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index b91850f1775..ec41909beec 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -11,10 +11,6 @@
color: var(--orange-600, $orange-600);
background-color: var(--orange-50, $orange-50);
border: 1px solid var(--border-color, $border-color);
- padding: 3px 12px;
- margin: auto;
- align-items: center;
- z-index: 1;
.with-performance-bar & {
top: $header-height + $performance-bar-height;
@@ -202,10 +198,6 @@
}
.build-job {
- &.active {
- font-weight: $gl-font-weight-bold;
- }
-
&.retried {
background-color: var(--gray-10, $gray-10);
}
diff --git a/app/assets/stylesheets/page_bundles/dev_ops_report.scss b/app/assets/stylesheets/page_bundles/dev_ops_report.scss
deleted file mode 100644
index 5c6019efce6..00000000000
--- a/app/assets/stylesheets/page_bundles/dev_ops_report.scss
+++ /dev/null
@@ -1,261 +0,0 @@
-@import 'mixins_and_variables_and_functions';
-
-$space-between-cards: 8px;
-
-.devops-empty svg {
- margin: 64px auto 32px;
- max-width: 420px;
-}
-
-.devops-header {
- margin-top: $gl-padding;
- margin-bottom: $gl-padding;
- padding: 0 4px;
- display: flex;
- align-items: center;
-
- .devops-header-title {
- font-size: 48px;
- line-height: 1;
- margin: 0;
- }
-
- .devops-header-subtitle {
- font-size: 22px;
- line-height: 1;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- margin-left: 8px;
- font-weight: $gl-font-weight-normal;
-
- .devops-header-icon {
- vertical-align: px-to-rem(-$gl-spacing-scale-1);
- }
-
- a {
- font-size: 18px;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
-
- &:hover {
- color: var(--blue-500, $blue-500);
- }
- }
- }
-}
-
-.devops-cards {
- display: flex;
- justify-content: center;
- flex-wrap: wrap;
-}
-
-.devops-card-wrapper {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- text-align: center;
- width: 50%;
- border-color: var(--border-color, $border-color);
- margin: 0 0 32px;
- padding: $space-between-cards / 2;
- position: relative;
-
- @include media-breakpoint-up(xs) {
- width: percentage(1 / 4);
- }
-
- @include media-breakpoint-up(sm) {
- width: percentage(1 / 5);
- }
-
- @include media-breakpoint-up(md) {
- width: percentage(1 / 6);
- }
-
- @include media-breakpoint-up(lg) {
- width: percentage(1 / 10);
- }
-}
-
-.devops-card {
- border: solid 1px var(--border-color, $border-color);
- border-radius: 3px;
- border-top-width: 3px;
- display: flex;
- flex-direction: column;
- flex-grow: 1;
-}
-
-.devops-card-low {
- border-top-color: var(--red-400, $red-400);
-
- .board-card-score-big {
- background-color: var(--red-50, $red-50);
- }
-}
-
-.devops-card-average {
- border-top-color: var(--orange-200, $orange-200);
-
- .board-card-score-big {
- background-color: var(--orange-50, $orange-50);
- }
-}
-
-.devops-card-high {
- border-top-color: var(--green-400, $green-400);
-
- .board-card-score-big {
- background-color: var(--green-50, $green-50);
- }
-}
-
-.devops-card-title {
- margin: $gl-padding auto auto;
- max-width: 100px;
-
- h3 {
- font-size: 14px;
- margin: 0 0 2px;
- }
-
- .light-text {
- font-size: 13px;
- line-height: 1.25;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- }
-}
-
-.board-card-scores {
- display: flex;
- justify-content: space-around;
- align-items: center;
- margin: $gl-padding $gl-btn-padding;
- line-height: 1;
-}
-
-.board-card-score {
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
-
- .board-card-score-name {
- font-size: 13px;
- margin-top: 4px;
- }
-}
-
-.board-card-score-value {
- font-size: 16px;
- color: var(--gl-text-color, $gl-text-color);
- font-weight: $gl-font-weight-normal;
-}
-
-.board-card-score-big {
- border-top: 2px solid var(--border-color, $border-color);
- border-bottom: 1px solid var(--border-color, $border-color);
- font-size: 22px;
- padding: 10px 0;
- font-weight: $gl-font-weight-normal;
-}
-
-.board-card-buttons {
- display: flex;
-
- > * {
- font-size: 16px;
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- padding: 10px;
- flex-grow: 1;
-
- &:hover {
- background-color: var(--border-color, $border-color);
- color: var(--border-color, $border-color);
- }
-
- + * {
- border-left: solid 1px var(--border-color, $border-color);
- }
- }
-}
-
-.devops-steps {
- margin-top: $gl-padding;
- height: 1px;
- min-width: 100%;
- justify-content: space-around;
- position: relative;
- background: var(--border-color, $border-color);
-}
-
-.devops-step {
- $step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%;
- @each $pos in $step-positions {
- $i: index($step-positions, $pos);
-
- &:nth-child(#{$i}) {
- left: $pos;
- }
- }
-
- position: absolute;
- transform-origin: 75% 50%;
- padding: 8px;
- height: 50px;
- width: 50px;
- border-radius: 3px;
- display: flex;
- flex-direction: column;
- align-items: center;
- border: solid 1px var(--border-color, $border-color);
- background: var(--white, $white);
- transform: translate(-50%, -50%);
- color: var(--gl-text-color-secondary, $gl-text-color-secondary);
- fill: var(--gl-text-color-secondary, $gl-text-color-secondary);
- box-shadow: 0 2px 4px var(--dropdown-shadow-color, $dropdown-shadow-color);
-
- &:hover {
- padding: 8px 10px;
- fill: currentColor;
- z-index: 100;
- height: auto;
- width: auto;
-
- .devops-step-title {
- max-height: 2em;
- opacity: 1;
- transition: opacity 0.2s;
- }
-
- svg {
- transform: scale(1.5);
- margin: $gl-btn-padding;
- }
- }
-
- svg {
- transition: transform 0.1s;
- width: 30px;
- height: 30px;
- min-height: 30px;
- min-width: 30px;
- }
-}
-
-.devops-step-title {
- max-height: 0;
- opacity: 0;
- text-transform: uppercase;
- margin: $gl-vert-padding 0 0;
- text-align: center;
- font-size: 12px;
-}
-
-.devops-high-score {
- color: var(--green-400, $green-400);
-}
-
-.devops-average-score {
- color: var(--orange-500, $orange-500);
-}
-
-.devops-low-score {
- color: var(--red-400, $red-400);
-}
diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss
index db4be3f18e8..4beb5edbe7b 100644
--- a/app/assets/stylesheets/page_bundles/jira_connect.scss
+++ b/app/assets/stylesheets/page_bundles/jira_connect.scss
@@ -45,3 +45,9 @@ $header-height: 40px;
margin-left: auto;
margin-right: auto;
}
+
+// needed for external_link
+svg.s16 {
+ width: 16px;
+ height: 16px;
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_conflicts.scss b/app/assets/stylesheets/page_bundles/merge_conflicts.scss
index d7473d2c942..9fe56fd337f 100644
--- a/app/assets/stylesheets/page_bundles/merge_conflicts.scss
+++ b/app/assets/stylesheets/page_bundles/merge_conflicts.scss
@@ -173,22 +173,5 @@
text-align: right;
padding: $gl-padding-top $gl-padding;
color: var(--gl-text-color, $gl-text-color);
-
- .discard-actions {
- display: inline-block;
- margin-left: 10px;
- }
- }
-
- .resolve-conflicts-form {
- h4 {
- margin-top: 0;
- }
-
- .resolve-info {
- @media(max-width: map-get($grid-breakpoints, lg)-1) {
- margin-bottom: $gl-padding;
- }
- }
}
}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 9fdc30359f8..5e9dd883635 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -7,6 +7,10 @@
.diff-files-holder {
flex: 1;
min-width: 0;
+
+ .vue-recycle-scroller__item-wrapper {
+ overflow: visible;
+ }
}
.with-system-header {
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 858e13fc558..03dd12ec230 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -38,18 +38,6 @@ $status-box-line-height: 26px;
color: var(--blue-600, $blue-600);
}
}
-
- .status-box {
- font-size: $tooltip-font-size;
- margin-top: 0;
- margin-right: $gl-padding-4;
- line-height: $status-box-line-height;
-
- @include media-breakpoint-down(xs) {
- line-height: unset;
- padding: $gl-padding-4 $gl-input-padding;
- }
- }
}
}
@@ -199,11 +187,6 @@ $status-box-line-height: 26px;
align-items: center;
flex-wrap: wrap;
- .status-box {
- margin-top: 0;
- order: 1;
- }
-
.milestone-buttons {
margin-left: auto;
order: 2;
diff --git a/app/assets/stylesheets/page_bundles/new_namespace.scss b/app/assets/stylesheets/page_bundles/new_namespace.scss
new file mode 100644
index 00000000000..60aa3c8f29f
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/new_namespace.scss
@@ -0,0 +1,28 @@
+@import 'mixins_and_variables_and_functions';
+
+$new-namespace-panel-illustration-width: 215px;
+$new-namespace-panel-height: 240px;
+
+.new-namespace-panel-illustration {
+ width: $new-namespace-panel-illustration-width;
+}
+
+.new-namespace-panel-wrapper {
+ @include media-breakpoint-down(md) {
+ width: 100%;
+ }
+ width: 50%;
+}
+
+.new-namespace-panel {
+ &:hover {
+ background-color: $gray-10;
+ }
+
+ color: $purple-700;
+ min-height: $new-namespace-panel-height;
+ text-align: center;
+ @include media-breakpoint-up(lg) {
+ text-align: left;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 5eaf91c3017..ddc638197ca 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -95,7 +95,7 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi
@include gl-font-weight-normal;
&.label-dark {
- @include gl-text-gray-900;
+ color: var(--gray-900, $gray-900);
}
&.label-bold {
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 2f3cf889549..c9171eb4fc7 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -306,11 +306,6 @@
}
}
- // ensure .build-content has hover style when action-icon is hovered
- .ci-job-dropdown-container:hover .build-content {
- @extend .build-content:hover;
- }
-
.ci-status-icon svg {
height: 24px;
width: 24px;
@@ -330,6 +325,7 @@
@include build-content();
}
+ .ci-job-dropdown-container:hover .build-content,
a.build-content:hover,
button.build-content:hover {
background-color: var(--gray-100, $gray-100);
@@ -409,7 +405,7 @@
fill: var(--gray-500, $gray-500);
}
- .spinner {
+ .gl-spinner {
top: 2px;
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index 6ef7f912ea9..ace91d197b6 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -182,11 +182,6 @@ button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle {
border-bottom-color: $border-color;
}
- &::after {
- margin-top: 1px;
- border-bottom-color: $white;
- }
-
/**
* Center dropdown menu in mini graph
*/
diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index 3e20ca9c62f..e7813e3b56e 100644
--- a/app/assets/stylesheets/page_bundles/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -8,8 +8,6 @@
.todos-list > .todo {
// workaround because we cannot use border-collapse
border-top: 1px solid transparent;
- display: flex;
- flex-direction: row;
&:hover {
background-color: var(--blue-50, $blue-50);
@@ -26,25 +24,6 @@
}
}
- .todo-avatar,
- .todo-actions {
- @include transition(opacity);
- flex: 0 0 auto;
- }
-
- .todo-actions {
- display: flex;
- justify-content: center;
- flex-direction: column;
- margin-left: 10px;
- min-width: 55px;
- }
-
- .todo-item {
- flex: 0 1 100%;
- min-width: 0;
- }
-
&.todo-pending.done-reversible {
&:hover {
border-color: var(--border-color, $border-color);
@@ -71,58 +50,22 @@
.todo-item {
@include transition(opacity);
- .todo-title {
- > .title-item {
- &:first-child {
- margin-left: 0;
- }
-
- &:last-child {
- margin-right: 0;
- }
- }
-
- .todo-label {
- flex: 0 1 auto;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
.status-box {
- margin: 0;
- float: none;
- display: inline-block;
- font-weight: $gl-font-weight-normal;
- padding: 0 5px;
line-height: inherit;
- font-size: 14px;
}
.todo-label,
.todo-project {
a {
- font-weight: $gl-font-weight-normal;
color: var(--blue-600, $blue-600);
}
}
.todo-body {
- .badge.badge-pill,
p {
color: var(--gl-text-color, $gl-text-color);
}
- .md {
- color: $gl-grayish-blue;
- font-size: $gl-font-size;
- }
-
- code {
- white-space: pre-wrap;
- }
-
pre {
border: 0;
background: var(--gray-50, $gray-50);
@@ -139,120 +82,13 @@
float: none;
}
- p:last-child {
- margin-bottom: 0;
- }
- }
-
- .gl-label-scoped {
- --label-inset-border: inset 0 0 0 1px currentColor;
- }
-}
-
-@include media-breakpoint-down(lg) {
- .todos-filters {
- .filter-categories {
- width: 75%;
-
- .filter-item {
- margin-bottom: 10px;
- }
+ .gl-label-scoped {
+ --label-inset-border: inset 0 0 0 1px currentColor;
}
- }
-}
-@include media-breakpoint-down(sm) {
- .container-fluid .todos-list-container {
- margin: 0 (-$gl-padding);
- }
-
- .todo {
- .avatar {
- display: none;
- }
- }
-
- .todo-item {
- .todo-title {
- margin-bottom: 10px;
-
- .todo-label {
- white-space: normal;
- }
- }
-
- .todo-body {
- margin: 0;
+ @include media-breakpoint-down(sm) {
border-left: 2px solid var(--border-color, $border-color);
padding-left: 10px;
}
}
-
- .todos-filters {
- .filter-categories {
- width: auto;
- }
-
- .dropdown-menu-toggle {
- width: 100%;
- }
-
- .dropdown-menu-toggle-sort {
- width: auto;
- }
- }
-}
-
-.todos-empty {
- display: flex;
- flex-direction: column;
- max-width: 900px;
- margin-left: auto;
- margin-right: auto;
-
- @include media-breakpoint-up(sm) {
- flex-direction: row;
- padding-top: 80px;
- }
-}
-
-.todos-empty-content {
- align-self: center;
- max-width: 480px;
-}
-
-.todos-empty-hero {
- width: 200px;
- margin-left: auto;
- margin-right: auto;
-
- @include media-breakpoint-up(sm) {
- width: 300px;
- margin-right: 0;
- order: 2;
- }
-}
-
-.todos-all-done {
- padding-top: 20px;
-
- @include media-breakpoint-up(sm) {
- padding-top: 50px;
- }
-
- > svg {
- display: block;
- max-width: 300px;
- margin: 0 auto 20px;
- }
-
- p {
- max-width: 470px;
- margin-left: auto;
- margin-right: auto;
- }
-
- a {
- font-weight: $gl-font-weight-bold;
- }
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 14cff5b038a..c177d0b74a2 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -37,10 +37,6 @@
.file-title {
@include gl-font-monospace;
- line-height: 35px;
- padding-top: 7px;
- padding-bottom: 7px;
- display: flex;
}
.editor-ref {
@@ -69,19 +65,15 @@
}
.file-buttons {
- display: flex;
flex: 1;
- justify-content: flex-end;
}
.soft-wrap-toggle {
- display: inline-block;
- vertical-align: top;
font-family: $regular-font;
- margin: 0 $btn-side-margin;
+ margin-left: $gl-padding-8;
.soft-wrap {
- display: block;
+ display: inline-flex;
}
.no-wrap {
@@ -94,7 +86,7 @@
}
.no-wrap {
- display: block;
+ display: inline-flex;
}
}
}
@@ -111,17 +103,21 @@
.new-file-path {
max-width: none;
width: 100%;
- margin-bottom: 3px;
+ margin-top: $gl-padding-8;
}
.file-buttons {
- display: block;
+ display: flex;
+ flex-direction: column;
width: 100%;
- margin-bottom: 10px;
+
+ .md-header-toolbar {
+ margin: $gl-padding 0;
+ }
.soft-wrap-toggle {
width: 100%;
- margin: 3px 0;
+ margin-left: 0;
}
@media(max-width: map-get($grid-breakpoints, md)-1) {
@@ -168,7 +164,6 @@
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
- .gitlab-ci-syntax-yml-selector,
.dockerfile-selector,
.template-type-selector,
.metrics-dashboard-selector {
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index c05216ac6e6..9182292ffd3 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -1,30 +1,30 @@
-.shortcut-mappings {
- font-size: 12px;
- color: $gray-700;
-
- tbody:first-child tr:first-child {
- padding-top: 0;
+.shortcut-help {
+ &-body {
+ height: 80vh;
+ overflow-y: scroll;
}
- th {
- padding-top: 15px;
- line-height: 1.5;
- color: $help-shortcut-header-color;
- text-align: left;
+ &-container {
+ column-count: 1;
+ @include media-breakpoint-up(md) {
+ column-count: 2;
+ }
+ column-gap: 1rem;
}
- td {
- padding-top: 3px;
- padding-bottom: 3px;
- vertical-align: top;
- line-height: 20px;
- }
+ &-mapping {
+ overflow: hidden;
+ break-inside: avoid;
+
+ &-title {
+ margin-left: 40%;
+ }
- .shortcut {
- padding-right: 10px;
- color: $gray-300;
- text-align: right;
- white-space: nowrap;
+ kbd {
+ margin: 0.1rem 0;
+ line-height: unset;
+ font-size: unset;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b9f5a427a24..0437fa19752 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -217,7 +217,6 @@
.title {
color: $gl-text-color;
- margin-bottom: $gl-padding-4;
line-height: $gl-line-height-20;
.avatar {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 59768f4cda8..c025d8569a7 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -369,10 +369,6 @@ table {
.btn {
float: none;
width: 100%;
-
- &:not(:last-child) {
- margin-bottom: 10px;
- }
}
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 801dd44be8e..01739c7eb3e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -372,7 +372,7 @@ $system-note-svg-size: 16px;
top: $mr-tabs-height + $header-height;
.with-performance-bar & {
- top: 126px;
+ top: 123px;
}
}
@@ -672,6 +672,7 @@ $system-note-svg-size: 16px;
align-items: center;
margin-left: 10px;
color: $gray-400;
+ margin-top: -4px;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 16f96ebadc9..dfd64d0773c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -237,11 +237,6 @@
line-height: 34px;
margin: 0;
- > li + li::before {
- padding: 0 3px;
- color: $gray-300;
- }
-
a {
color: $gl-text-color;
}
@@ -1032,11 +1027,6 @@ pre.light-well {
}
}
-.issuable-footer {
- padding-top: $gl-padding;
- padding-bottom: 37px;
-}
-
.project-ci-linter {
.ci-editor {
height: 400px;
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index 346b3f61caa..7d74070b4f2 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -11,10 +11,6 @@
.trigger-actions {
white-space: nowrap;
-
- .btn {
- margin-left: 10px;
- }
}
.auto-devops-card {
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 11b4bde74a6..9d98fe5c739 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -70,6 +70,7 @@ $indigo-700: #a6a6de;
$indigo-800: #d1d1f0;
$indigo-900: #ebebfa;
$indigo-950: #f7f7ff;
+$indigo-900-alpha-008: rgba($indigo-900, 0.08);
$gray-lightest: #222;
$gray-light: $gray-50;
@@ -160,6 +161,7 @@ body.gl-dark {
--indigo-800: #{$indigo-800};
--indigo-900: #{$indigo-900};
--indigo-950: #{$indigo-950};
+ --indigo-900-alpha-008: #{$indigo-900-alpha-008};
--gl-text-color: #{$gray-900};
--border-color: #{$border-color};
@@ -232,9 +234,7 @@ $well-inner-border: $gray-200;
}
// white-ish text for light labels
-// and for scoped label value (the right section)
-.gl-label-text-light.gl-label-text-light,
-.gl-label-text-dark + .gl-label-text-dark {
+.gl-label-text-light.gl-label-text-light {
color: $gray-900;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 024162eba3e..c22a1ae1187 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -172,3 +172,38 @@
width: 50%;
}
}
+
+.gl-sm-mr-3 {
+ @include media-breakpoint-up(sm) {
+ margin-right: $gl-spacing-scale-3;
+ }
+}
+
+.gl-mb-n3 {
+ margin-bottom: -$gl-spacing-scale-3;
+}
+
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408
+$gl-line-height-42: px-to-rem(42px);
+
+.gl-line-height-42 {
+ line-height: $gl-line-height-42;
+}
+
+.gl-w-grid-size-30 {
+ width: $grid-size * 30;
+}
+
+.gl-w-grid-size-40 {
+ width: $grid-size * 40;
+}
+
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
+.gl-max-w-none\! {
+ max-width: none !important;
+}
+
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
+.gl-max-h-none\! {
+ max-height: none !important;
+}
diff --git a/app/channels/graphql_channel.rb b/app/channels/graphql_channel.rb
new file mode 100644
index 00000000000..d364cc2b64b
--- /dev/null
+++ b/app/channels/graphql_channel.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# This is based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.8/lib/graphql/subscriptions/action_cable_subscriptions.rb#L19-L82
+# modified to work with our own ActionCableLink client
+
+class GraphqlChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
+ def subscribed
+ @subscription_ids = []
+
+ query = params['query']
+ variables = Gitlab::Graphql::Variables.new(params['variables']).to_h
+ operation_name = params['operationName']
+
+ result = GitlabSchema.execute(
+ query,
+ context: context,
+ variables: variables,
+ operation_name: operation_name
+ )
+
+ payload = {
+ result: result.to_h,
+ more: result.subscription?
+ }
+
+ # Track the subscription here so we can remove it
+ # on unsubscribe.
+ if result.context[:subscription_id]
+ @subscription_ids << result.context[:subscription_id]
+ end
+
+ transmit(payload)
+ end
+
+ def unsubscribed
+ @subscription_ids.each do |sid|
+ GitlabSchema.subscriptions.delete_subscription(sid)
+ end
+ end
+
+ rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
+ transmit({ errors: [{ message: exception.message }] })
+ end
+
+ private
+
+ # When modifying the context, also update GraphqlController#context if needed
+ # so that we have similar context when executing queries, mutations, and subscriptions
+ #
+ # Objects added to the context may also need to be reloaded in
+ # `Subscriptions::BaseSubscription` so that they are not stale
+ def context
+ # is_sessionless_user is always false because we only support cookie auth in ActionCable
+ { channel: self, current_user: current_user, is_sessionless_user: false }
+ end
+end
diff --git a/app/channels/issues_channel.rb b/app/channels/issues_channel.rb
deleted file mode 100644
index 5f3909b7716..00000000000
--- a/app/channels/issues_channel.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class IssuesChannel < ApplicationCable::Channel
- def subscribed
- project = Project.find_by_full_path(params[:project_path])
- return reject unless project
-
- issue = project.issues.find_by_iid(params[:iid])
- return reject unless issue && Ability.allowed?(current_user, :read_issue, issue)
-
- stream_for issue
- end
-end
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index 956e03cef07..bab2d5639a7 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -9,4 +9,4 @@ class Admin::ApplicationController < ApplicationController
layout 'admin'
end
-Admin::ApplicationController.prepend_if_ee('EE::Admin::ApplicationController')
+Admin::ApplicationController.prepend_mod_with('Admin::ApplicationController')
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 646a6dffd10..80cb04ac496 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -49,7 +49,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def integrations
return not_found unless instance_level_integrations?
- @integrations = Service.find_or_initialize_all_non_project_specific(Service.for_instance).sort_by(&:title)
+ @integrations = Integration.find_or_initialize_all_non_project_specific(Integration.for_instance).sort_by(&:title)
end
def update
@@ -292,4 +292,4 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
-Admin::ApplicationSettingsController.prepend_if_ee('EE::Admin::ApplicationSettingsController')
+Admin::ApplicationSettingsController.prepend_mod_with('Admin::ApplicationSettingsController')
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
index 9bb73c822b0..c29b5224b09 100644
--- a/app/controllers/admin/cohorts_controller.rb
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -6,6 +6,6 @@ class Admin::CohortsController < Admin::ApplicationController
# Backwards compatibility. Remove it and routing in 14.0
# @see https://gitlab.com/gitlab-org/gitlab/-/issues/299303
def index
- redirect_to admin_users_path(tab: 'cohorts')
+ redirect_to cohorts_admin_users_path
end
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index da89276f5eb..46e5a508a1b 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -24,4 +24,4 @@ class Admin::DashboardController < Admin::ApplicationController
end
end
-Admin::DashboardController.prepend_if_ee('EE::Admin::DashboardController')
+Admin::DashboardController.prepend_mod_with('Admin::DashboardController')
diff --git a/app/controllers/admin/dev_ops_report_controller.rb b/app/controllers/admin/dev_ops_report_controller.rb
index 4178e51fb13..a235af7c538 100644
--- a/app/controllers/admin/dev_ops_report_controller.rb
+++ b/app/controllers/admin/dev_ops_report_controller.rb
@@ -24,4 +24,4 @@ class Admin::DevOpsReportController < Admin::ApplicationController
end
end
-Admin::DevOpsReportController.prepend_if_ee('EE::Admin::DevOpsReportController')
+Admin::DevOpsReportController.prepend_mod_with('Admin::DevOpsReportController')
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index e14cfc236cf..5b33ee78e8c 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -117,4 +117,4 @@ class Admin::GroupsController < Admin::ApplicationController
end
end
-Admin::GroupsController.prepend_if_ee('EE::Admin::GroupsController')
+Admin::GroupsController.prepend_mod_with('Admin::GroupsController')
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index e013b5fbd72..5733929c25e 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -14,4 +14,4 @@ class Admin::HealthCheckController < Admin::ApplicationController
end
end
-Admin::HealthCheckController.prepend_if_ee('EE::Admin::HealthCheckController')
+Admin::HealthCheckController.prepend_mod_with('Admin::HealthCheckController')
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index 4247446365c..316e6d9aa74 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -11,7 +11,7 @@ class Admin::IntegrationsController < Admin::ApplicationController
private
def find_or_initialize_non_project_specific_integration(name)
- Service.find_or_initialize_non_project_specific_integration(name, instance: true)
+ Integration.find_or_initialize_non_project_specific_integration(name, instance: true)
end
def scoped_edit_integration_path(integration)
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index be63bf4c7ce..6cc11b40de0 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -47,7 +47,7 @@ class Admin::LabelsController < Admin::ApplicationController
format.html do
redirect_to admin_labels_path, status: :found, notice: _('Label was removed')
end
- format.js
+ format.js { head :ok }
end
end
diff --git a/app/controllers/admin/plan_limits_controller.rb b/app/controllers/admin/plan_limits_controller.rb
index 0a5cdc06d61..88bc5ea0198 100644
--- a/app/controllers/admin/plan_limits_controller.rb
+++ b/app/controllers/admin/plan_limits_controller.rb
@@ -35,6 +35,7 @@ class Admin::PlanLimitsController < Admin::ApplicationController
npm_max_file_size
nuget_max_file_size
pypi_max_file_size
+ terraform_module_max_file_size
generic_packages_max_file_size
])
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 39718793c1d..6fd1e9bb70e 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -89,4 +89,4 @@ class Admin::ProjectsController < Admin::ApplicationController
end
end
-Admin::ProjectsController.prepend_if_ee('EE::Admin::ProjectsController')
+Admin::ProjectsController.prepend_mod_with('Admin::ProjectsController')
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 576b148fbff..40ec68c1d46 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -7,9 +7,11 @@ class Admin::RunnersController < Admin::ApplicationController
feature_category :continuous_integration
+ NUMBER_OF_RUNNERS_PER_PAGE = 30
+
def index
finder = Ci::RunnersFinder.new(current_user: current_user, params: params)
- @runners = finder.execute
+ @runners = finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
@active_runners_count = Ci::Runner.online.count
@sort = finder.sort_key
end
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 9f951e838c8..d34773ee4dc 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -1,28 +1,28 @@
# frozen_string_literal: true
class Admin::ServicesController < Admin::ApplicationController
- include ServiceParams
+ include Integrations::Params
- before_action :service, only: [:edit, :update]
+ before_action :integration, only: [:edit, :update]
before_action :disable_query_limiting, only: [:index]
feature_category :integrations
def index
- @activated_services = Service.for_template.active.sort_by(&:title)
- @existing_instance_types = Service.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
+ @activated_services = Integration.for_template.active.sort_by(&:title)
+ @existing_instance_types = Integration.for_instance.pluck(:type) # rubocop: disable CodeReuse/ActiveRecord
end
def edit
- if service.nil? || Service.instance_exists_for?(service.type)
+ if integration.nil? || Integration.instance_exists_for?(integration.type)
redirect_to admin_application_settings_services_path,
alert: "Service is unknown or it doesn't exist"
end
end
def update
- if service.update(service_params[:service])
- PropagateServiceTemplateWorker.perform_async(service.id) if service.active? # rubocop:disable CodeReuse/Worker
+ if integration.update(integration_params[:integration])
+ PropagateServiceTemplateWorker.perform_async(integration.id) if integration.active? # rubocop:disable CodeReuse/Worker
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
@@ -34,9 +34,11 @@ class Admin::ServicesController < Admin::ApplicationController
private
# rubocop: disable CodeReuse/ActiveRecord
- def service
- @service ||= Service.find_by(id: params[:id], template: true)
+ def integration
+ @integration ||= Integration.find_by(id: params[:id], template: true)
+ @service ||= @integration # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/329759
end
+ alias_method :service, :integration
# rubocop: enable CodeReuse/ActiveRecord
def disable_query_limiting
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 8a090c8ef10..2e9229db56c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -4,22 +4,29 @@ class Admin::UsersController < Admin::ApplicationController
include RoutableActions
include Analytics::UniqueVisitsHelper
- before_action :user, except: [:index, :new, :create]
+ before_action :user, except: [:index, :cohorts, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
+ before_action :check_ban_user_feature_flag, only: [:ban]
feature_category :users
+ PAGINATION_WITH_COUNT_LIMIT = 1000
+
def index
+ return redirect_to cohorts_admin_users_path if params[:tab] == 'cohorts'
+
@users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = users_with_included_associations(@users)
@users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page])
+ @users = @users.without_count if paginate_without_count?
+ end
+ def cohorts
@cohorts = load_cohorts
-
- track_cohorts_visit if params[:tab] == 'cohorts'
+ track_cohorts_visit
end
def show
@@ -124,6 +131,24 @@ class Admin::UsersController < Admin::ApplicationController
end
end
+ def ban
+ result = Users::BanService.new(current_user).execute(user)
+
+ if result[:status] == :success
+ redirect_back_or_admin_user(notice: _("Successfully banned"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not banned"))
+ end
+ end
+
+ def unban
+ if update_user { |user| user.activate }
+ redirect_back_or_admin_user(notice: _("Successfully unbanned"))
+ else
+ redirect_back_or_admin_user(alert: _("Error occurred. User was not unbanned"))
+ end
+ end
+
def unlock
if update_user { |user| user.unlock_access! }
redirect_back_or_admin_user(alert: _("Successfully unlocked"))
@@ -228,6 +253,12 @@ class Admin::UsersController < Admin::ApplicationController
protected
+ def paginate_without_count?
+ counts = Gitlab::Database::Count.approximate_counts([User])
+
+ counts[User] > PAGINATION_WITH_COUNT_LIMIT
+ end
+
def users_with_included_associations(users)
users.includes(:authorized_projects) # rubocop: disable CodeReuse/ActiveRecord
end
@@ -313,18 +344,20 @@ class Admin::UsersController < Admin::ApplicationController
access_denied! unless Gitlab.config.gitlab.impersonation_enabled
end
+ def check_ban_user_feature_flag
+ access_denied! unless Feature.enabled?(:ban_user_feature_flag)
+ end
+
def log_impersonation_event
Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username })
end
def load_cohorts
- if Gitlab::CurrentSettings.usage_ping_enabled
- cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
- CohortsService.new.execute
- end
-
- CohortsSerializer.new.represent(cohorts_results)
+ cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
+ CohortsService.new.execute
end
+
+ CohortsSerializer.new.represent(cohorts_results)
end
def track_cohorts_visit
@@ -334,4 +367,4 @@ class Admin::UsersController < Admin::ApplicationController
end
end
-Admin::UsersController.prepend_if_ee('EE::Admin::UsersController')
+Admin::UsersController.prepend_mod_with('Admin::UsersController')
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 607f3435394..00b9fb1060d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base
include Gitlab::Logging::CloudflareHelper
include Gitlab::Utils::StrongMemoize
include ::Gitlab::WithFeatureCategory
+ include FlocOptOut
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
@@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
redirect_back(fallback_location: default, **options)
end
+ def check_if_gl_com_or_dev
+ render_404 unless ::Gitlab.dev_env_or_com?
+ end
+
def not_found
render_404
end
@@ -207,13 +212,13 @@ class ApplicationController < ActionController::Base
end
respond_to do |format|
- format.any { head status }
format.html do
render template,
layout: "errors",
status: status,
locals: { message: message }
end
+ format.any { head status }
end
end
@@ -223,8 +228,8 @@ class ApplicationController < ActionController::Base
def render_403
respond_to do |format|
- format.any { head :forbidden }
format.html { render "errors/access_denied", layout: "errors", status: :forbidden }
+ format.any { head :forbidden }
end
end
@@ -555,4 +560,4 @@ class ApplicationController < ActionController::Base
end
end
-ApplicationController.prepend_ee_mod
+ApplicationController.prepend_mod
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 79e45bcf929..1c07245da08 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -71,4 +71,4 @@ class AutocompleteController < ApplicationController
end
end
-AutocompleteController.prepend_if_ee('EE::AutocompleteController')
+AutocompleteController.prepend_mod_with('AutocompleteController')
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 347bf1f4fa8..003ed45adb5 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -27,7 +27,9 @@ module Boards
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = issues_from(list_service)
- Issue.move_nulls_to_end(issues) if Gitlab::Database.read_write?
+ if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
+ Issue.move_nulls_to_end(issues)
+ end
render_issues(issues, list_service.metadata)
end
@@ -158,4 +160,4 @@ module Boards
end
end
-Boards::IssuesController.prepend_if_ee('EE::Boards::IssuesController')
+Boards::IssuesController.prepend_mod_with('Boards::IssuesController')
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 19a4508c061..8ab8337a3ad 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -33,10 +33,10 @@ module Boards
service = Boards::Lists::UpdateService.new(board_parent, current_user, update_list_params)
result = service.execute(list)
- if result[:status] == :success
+ if result.success?
head :ok
else
- head result[:http_status]
+ head result.http_status
end
end
@@ -99,4 +99,4 @@ module Boards
end
end
-Boards::ListsController.prepend_if_ee('EE::Boards::ListsController')
+Boards::ListsController.prepend_mod_with('Boards::ListsController')
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index c64301f72ba..32de9e69c85 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -62,6 +62,7 @@ class Clusters::ClustersController < Clusters::BaseController
def show
if params[:tab] == 'integrations'
@prometheus_integration = Clusters::IntegrationPresenter.new(@cluster.find_or_build_integration_prometheus)
+ @elastic_stack_integration = Clusters::IntegrationPresenter.new(@cluster.find_or_build_integration_elastic_stack)
end
end
@@ -362,4 +363,4 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
-Clusters::ClustersController.prepend_if_ee('EE::Clusters::ClustersController')
+Clusters::ClustersController.prepend_mod_with('Clusters::ClustersController')
diff --git a/app/controllers/clusters/integrations_controller.rb b/app/controllers/clusters/integrations_controller.rb
index a8c7eb10136..17884a55242 100644
--- a/app/controllers/clusters/integrations_controller.rb
+++ b/app/controllers/clusters/integrations_controller.rb
@@ -24,7 +24,7 @@ module Clusters
end
def cluster_integration_params
- params.require(:integration).permit(:application_type, :enabled)
+ params.permit(integration: [:enabled, :application_type]).require(:integration)
end
def cluster
diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb
index cb66c1a055d..5601b7a7f79 100644
--- a/app/controllers/concerns/accepts_pending_invitations.rb
+++ b/app/controllers/concerns/accepts_pending_invitations.rb
@@ -6,7 +6,15 @@ module AcceptsPendingInvitations
def accept_pending_invitations
return unless resource.active_for_authentication?
- clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
+ if resource.pending_invitations.load.any?
+ resource.accept_pending_invitations!
+ clear_stored_location_for_resource
+ after_pending_invitations_hook
+ end
+ end
+
+ def after_pending_invitations_hook
+ # no-op
end
def clear_stored_location_for_resource
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 87555a28eb8..4f4b204def8 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -177,4 +177,4 @@ module AuthenticatesWithTwoFactor
end
end
-AuthenticatesWithTwoFactor.prepend_if_ee('EE::AuthenticatesWithTwoFactor')
+AuthenticatesWithTwoFactor.prepend_mod_with('AuthenticatesWithTwoFactor')
diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb
index 79e6f027c2f..2f9edfad12d 100644
--- a/app/controllers/concerns/boards_actions.rb
+++ b/app/controllers/concerns/boards_actions.rb
@@ -7,12 +7,10 @@ module BoardsActions
included do
include BoardsResponses
+ before_action :authorize_read_board!, only: [:index, :show]
before_action :boards, only: :index
before_action :board, only: :show
before_action :push_licensed_features, only: [:index, :show]
- before_action do
- push_frontend_feature_flag(:not_issuable_queries, parent, default_enabled: true)
- end
end
def index
@@ -21,7 +19,7 @@ module BoardsActions
def show
# Add / update the board in the recent visits table
- Boards::Visits::CreateService.new(parent, current_user).execute(board) if request.format.html?
+ board_visit_service.new(parent, current_user).execute(board) if request.format.html?
respond_with_board
end
@@ -54,6 +52,10 @@ module BoardsActions
board_klass.to_type
end
+ def board_visit_service
+ Boards::Visits::CreateService
+ end
+
def serializer
BoardSerializer.new(current_user: current_user)
end
@@ -63,4 +65,4 @@ module BoardsActions
end
end
-BoardsActions.prepend_if_ee('EE::BoardsActions')
+BoardsActions.prepend_mod_with('BoardsActions')
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
index 7307b7b4f8f..eb7392648a1 100644
--- a/app/controllers/concerns/boards_responses.rb
+++ b/app/controllers/concerns/boards_responses.rb
@@ -91,4 +91,4 @@ module BoardsResponses
end
end
-BoardsResponses.prepend_if_ee('EE::BoardsResponses')
+BoardsResponses.prepend_mod_with('BoardsResponses')
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 50e340dc9b1..b74e343f90b 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -43,4 +43,4 @@ module CycleAnalyticsParams
end
end
-CycleAnalyticsParams.prepend_if_ee('EE::CycleAnalyticsParams')
+CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index bf38e4ad117..c67e73d4e78 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -72,4 +72,4 @@ module EnforcesTwoFactorAuthentication
end
end
-EnforcesTwoFactorAuthentication.prepend_if_ee('EE::EnforcesTwoFactorAuthentication')
+EnforcesTwoFactorAuthentication.prepend_mod_with('EnforcesTwoFactorAuthentication')
diff --git a/app/controllers/concerns/floc_opt_out.rb b/app/controllers/concerns/floc_opt_out.rb
new file mode 100644
index 00000000000..3039af02bbb
--- /dev/null
+++ b/app/controllers/concerns/floc_opt_out.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module FlocOptOut
+ extend ActiveSupport::Concern
+
+ included do
+ after_action :set_floc_opt_out_header, unless: :floc_enabled?
+ end
+
+ def floc_enabled?
+ Gitlab::CurrentSettings.floc_enabled
+ end
+
+ def set_floc_opt_out_header
+ response.headers['Permissions-Policy'] = 'interest-cohort=()'
+ end
+end
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
new file mode 100644
index 00000000000..10122b4c77b
--- /dev/null
+++ b/app/controllers/concerns/integrations/params.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Integrations
+ module Params
+ extend ActiveSupport::Concern
+
+ ALLOWED_PARAMS_CE = [
+ :active,
+ :add_pusher,
+ :alert_events,
+ :api_key,
+ :api_url,
+ :bamboo_url,
+ :branches_to_be_notified,
+ :labels_to_be_notified,
+ :labels_to_be_notified_behavior,
+ :build_key,
+ :build_type,
+ :ca_pem,
+ :channel,
+ :channels,
+ :color,
+ :colorize_messages,
+ :comment_on_event_enabled,
+ :comment_detail,
+ :confidential_issues_events,
+ :confluence_url,
+ :datadog_site,
+ :datadog_env,
+ :datadog_service,
+ :default_irc_uri,
+ :device,
+ :disable_diffs,
+ :drone_url,
+ :enable_ssl_verification,
+ :external_wiki_url,
+ :google_iap_service_account_json,
+ :google_iap_audience_client_id,
+ :inherit_from_id,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events,
+ :issues_url,
+ :jenkins_url,
+ :jira_issue_transition_automatic,
+ :jira_issue_transition_id,
+ :manual_configuration,
+ :merge_requests_events,
+ :mock_service_url,
+ :namespace,
+ :new_issue_url,
+ :notify_only_broken_pipelines,
+ :password,
+ :priority,
+ :project_key,
+ :project_name,
+ :project_url,
+ :recipients,
+ :restrict_to_branch,
+ :room,
+ :send_from_committer_email,
+ :server,
+ :server_host,
+ :server_port,
+ :sound,
+ :subdomain,
+ :teamcity_url,
+ :token,
+ :type,
+ :url,
+ :user_key,
+ :username,
+ :webhook
+ ].freeze
+
+ # Parameters to ignore if no value is specified
+ FILTER_BLANK_PARAMS = [:password].freeze
+
+ def integration_params
+ dynamic_params = @integration.event_channel_names + @integration.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ allowed = allowed_integration_params + dynamic_params
+ return_value = params.permit(:id, integration: allowed, service: allowed)
+ return_value[:integration] ||= return_value.delete(:service)
+ param_values = return_value[:integration]
+
+ if param_values.is_a?(ActionController::Parameters)
+ FILTER_BLANK_PARAMS.each do |param|
+ param_values.delete(param) if param_values[param].blank?
+ end
+ end
+
+ return_value
+ end
+
+ def allowed_integration_params
+ ALLOWED_PARAMS_CE
+ end
+ end
+end
+
+Integrations::Params.prepend_mod_with('Integrations::Params')
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index a3ea39d9c3d..f5a3ec913c2 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -4,7 +4,7 @@ module IntegrationsActions
extend ActiveSupport::Concern
included do
- include ServiceParams
+ include Integrations::Params
before_action :integration, only: [:edit, :update, :test]
end
@@ -14,7 +14,7 @@ module IntegrationsActions
end
def update
- saved = integration.update(service_params[:service])
+ saved = integration.update(integration_params[:integration])
respond_to do |format|
format.html do
@@ -49,9 +49,7 @@ module IntegrationsActions
private
def integration
- # Using instance variable `@service` still required as it's used in ServiceParams.
- # Should be removed once that is refactored to use `@integration`.
- @integration = @service ||= find_or_initialize_non_project_specific_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @integration ||= find_or_initialize_non_project_specific_integration(params[:id])
end
def success_message
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index a35bc19aa37..b803be67d2e 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -46,4 +46,4 @@ module InternalRedirect
end
end
-InternalRedirect.prepend_if_ee('EE::InternalRedirect')
+InternalRedirect.prepend_mod_with('InternalRedirect')
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 57d4203ad43..929e60a9e77 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -8,9 +8,6 @@ module IssuableActions
before_action :authorize_destroy_issuable!, only: :destroy
before_action :check_destroy_confirmation!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
- before_action do
- push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true)
- end
end
def show
@@ -64,7 +61,7 @@ module IssuableActions
end
def destroy
- Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
+ Issuable::DestroyService.new(project: issuable.project, current_user: current_user).execute(issuable)
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
@@ -262,4 +259,4 @@ module IssuableActions
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
-IssuableActions.prepend_if_ee('EE::IssuableActions')
+IssuableActions.prepend_mod_with('IssuableActions')
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3f5f3b6e9df..d2d2e656af8 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -158,4 +158,4 @@ module IssuableCollections
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
-IssuableCollections.prepend_if_ee('EE::IssuableCollections')
+IssuableCollections.prepend_mod_with('IssuableCollections')
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 7ed66027da3..ca2979a5a29 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -32,10 +32,6 @@ module IssuableCollectionsAction
private
- def set_not_query_feature_flag(object = nil)
- push_frontend_feature_flag(:not_issuable_queries, object, default_enabled: true)
- end
-
def sorting_field
case action_name
when 'issues'
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index bc3fd32759f..55e0ed8cd42 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -136,4 +136,4 @@ module LfsRequest
end
end
-LfsRequest.prepend_if_ee('EE::LfsRequest')
+LfsRequest.prepend_mod_with('LfsRequest')
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 7bbee8ba79e..20861afbb88 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -186,3 +186,5 @@ module MembershipActions
end
end
end
+
+MembershipActions.prepend_mod_with('MembershipActions')
diff --git a/app/controllers/concerns/page_limiter.rb b/app/controllers/concerns/page_limiter.rb
index 3c280fa4f12..362b02e5856 100644
--- a/app/controllers/concerns/page_limiter.rb
+++ b/app/controllers/concerns/page_limiter.rb
@@ -46,7 +46,7 @@ module PageLimiter
if params[:page].present? && params[:page].to_i > max_page_number
record_page_limit_interception
- raise PageOutOfBoundsError.new(max_page_number)
+ raise PageOutOfBoundsError, max_page_number
end
end
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index 4ea07c814ef..f1f5a1179c9 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -23,6 +23,7 @@ module RendersCommits
def prepare_commits_for_rendering(commits)
commits.each(&:lazy_author) # preload commits' authors
+ commits.each(&:lazy_latest_pipeline)
Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index c92b1cecaaa..e98c1a30887 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -35,6 +35,6 @@ module RequiresWhitelistedMonitoringClient
end
def render_404
- render file: Rails.root.join('public', '404'), layout: false, status: '404'
+ render "errors/not_found", layout: "errors", status: :not_found
end
end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index bc2e7fba288..7257378f465 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -56,4 +56,4 @@ module RoutableActions
end
end
-RoutableActions.prepend_if_ee('EE::RoutableActions')
+RoutableActions.prepend_mod_with('RoutableActions')
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
deleted file mode 100644
index 7c57d321c80..00000000000
--- a/app/controllers/concerns/service_params.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-# frozen_string_literal: true
-
-module ServiceParams
- extend ActiveSupport::Concern
-
- ALLOWED_PARAMS_CE = [
- :active,
- :add_pusher,
- :alert_events,
- :api_key,
- :api_url,
- :api_version,
- :bamboo_url,
- :branches_to_be_notified,
- :labels_to_be_notified,
- :build_key,
- :build_type,
- :ca_pem,
- :channel,
- :channels,
- :color,
- :colorize_messages,
- :comment_on_event_enabled,
- :comment_detail,
- :confidential_issues_events,
- :confluence_url,
- :datadog_site,
- :datadog_env,
- :datadog_service,
- :default_irc_uri,
- :device,
- :disable_diffs,
- :drone_url,
- :enable_ssl_verification,
- :external_wiki_url,
- :google_iap_service_account_json,
- :google_iap_audience_client_id,
- :inherit_from_id,
- # We're using `issues_events` and `merge_requests_events`
- # in the view so we still need to explicitly state them
- # here. `Service#event_names` would only give
- # `issue_events` and `merge_request_events` (singular!)
- # See app/helpers/services_helper.rb for how we
- # make those event names plural as special case.
- :issues_events,
- :issues_url,
- :jenkins_url,
- :jira_issue_transition_automatic,
- :jira_issue_transition_id,
- :manual_configuration,
- :merge_requests_events,
- :mock_service_url,
- :namespace,
- :new_issue_url,
- :notify,
- :notify_only_broken_pipelines,
- :password,
- :priority,
- :project_key,
- :project_name,
- :project_url,
- :recipients,
- :restrict_to_branch,
- :room,
- :send_from_committer_email,
- :server,
- :server_host,
- :server_port,
- :sound,
- :subdomain,
- :teamcity_url,
- :token,
- :type,
- :url,
- :user_key,
- :username,
- :webhook
- ].freeze
-
- # Parameters to ignore if no value is specified
- FILTER_BLANK_PARAMS = [:password].freeze
-
- def service_params
- dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables
- service_params = params.permit(:id, service: allowed_service_params + dynamic_params)
-
- if service_params[:service].is_a?(ActionController::Parameters)
- FILTER_BLANK_PARAMS.each do |param|
- service_params[:service].delete(param) if service_params[:service][param].blank?
- end
- end
-
- service_params
- end
-
- def allowed_service_params
- ALLOWED_PARAMS_CE
- end
-end
-
-ServiceParams.prepend_if_ee('EE::ServiceParams')
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 60ff0a12d0c..fc4f9aa3409 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -115,9 +115,6 @@ module WikiActions
@error = response.message
render 'shared/wikis/edit'
end
- rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
- @error = e.message
- render 'shared/wikis/edit'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -141,8 +138,8 @@ module WikiActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def history
if page
- @page_versions = Kaminari.paginate_array(page.versions(page: params[:page].to_i),
- total_count: page.count_versions)
+ @commits = Kaminari.paginate_array(page.versions(page: params[:page].to_i),
+ total_count: page.count_versions)
.page(params[:page])
render 'shared/wikis/history'
diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb
index 93ded59900d..dc2265e063a 100644
--- a/app/controllers/concerns/with_performance_bar.rb
+++ b/app/controllers/concerns/with_performance_bar.rb
@@ -20,12 +20,12 @@ module WithPerformanceBar
end
def cookie_or_default_value
- return false unless Gitlab::PerformanceBar.enabled_for_user?(current_user)
+ cookie_enabled = if cookies[:perf_bar_enabled].present?
+ cookies[:perf_bar_enabled] == 'true'
+ else
+ cookies[:perf_bar_enabled] = 'true' if Rails.env.development?
+ end
- if cookies[:perf_bar_enabled].present?
- cookies[:perf_bar_enabled] == 'true'
- else
- cookies[:perf_bar_enabled] = 'true' if Rails.env.development?
- end
+ cookie_enabled && Gitlab::PerformanceBar.allowed_for_user?(current_user)
end
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index c42c9827eaf..e82500912fa 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -27,7 +27,7 @@ class ConfirmationsController < Devise::ConfirmationsController
else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] = flash[:notice] + _(" Please sign in.")
- new_session_path(:user, anchor: 'login-pane')
+ new_session_path(:user, anchor: 'login-pane', invite_email: resource.email)
end
end
@@ -36,4 +36,4 @@ class ConfirmationsController < Devise::ConfirmationsController
end
end
-ConfirmationsController.prepend_if_ee('EE::ConfirmationsController')
+ConfirmationsController.prepend_mod_with('ConfirmationsController')
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index aa3592ff209..7cb39625371 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -116,4 +116,4 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
end
-Dashboard::ProjectsController.prepend_if_ee('EE::Dashboard::ProjectsController')
+Dashboard::ProjectsController.prepend_mod_with('Dashboard::ProjectsController')
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 29cb60ad3cc..227dd0591d4 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -11,11 +11,10 @@ class DashboardController < Dashboard::ApplicationController
before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests]
before_action :check_filters_presence!, only: [:issues, :merge_requests]
- before_action :set_not_query_feature_flag
respond_to :html
- feature_category :audit_events, [:activity]
+ feature_category :users, [:activity]
feature_category :issue_tracking, [:issues, :issues_calendar]
feature_category :code_review, [:merge_requests]
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index f6671f7250f..5ef973e9bf3 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -122,4 +122,4 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
-Explore::ProjectsController.prepend_if_ee('EE::Explore::ProjectsController')
+Explore::ProjectsController.prepend_mod_with('Explore::ProjectsController')
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 38bfb5ef2f8..725d8b62c77 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -109,6 +109,8 @@ class GraphqlController < ApplicationController
end
end
+ # When modifying the context, also update GraphqlChannel#context if needed
+ # so that we have similar context when executing queries, mutations, and subscriptions
def context
api_user = !!sessionless_user?
@context ||= {
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index a504d2ce991..a3bbfc8be0d 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -72,4 +72,4 @@ class Groups::ApplicationController < ApplicationController
end
end
-Groups::ApplicationController.prepend_if_ee('EE::Groups::ApplicationController')
+Groups::ApplicationController.prepend_mod_with('Groups::ApplicationController')
diff --git a/app/controllers/groups/autocomplete_sources_controller.rb b/app/controllers/groups/autocomplete_sources_controller.rb
new file mode 100644
index 00000000000..5270a718952
--- /dev/null
+++ b/app/controllers/groups/autocomplete_sources_controller.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+class Groups::AutocompleteSourcesController < Groups::ApplicationController
+ feature_category :subgroups, [:members]
+ feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
+ feature_category :code_review, [:merge_requests]
+
+ def members
+ render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
+ end
+
+ def issues
+ render json: issuable_serializer.represent(
+ autocomplete_service.issues(confidential_only: params[:confidential_only], issue_types: params[:issue_types]),
+ parent_group: @group
+ )
+ end
+
+ def merge_requests
+ render json: issuable_serializer.represent(autocomplete_service.merge_requests, parent_group: @group)
+ end
+
+ def labels
+ render json: autocomplete_service.labels_as_hash(target)
+ end
+
+ def commands
+ render json: autocomplete_service.commands(target)
+ end
+
+ def milestones
+ render json: autocomplete_service.milestones
+ end
+
+ private
+
+ def autocomplete_service
+ @autocomplete_service ||= ::Groups::AutocompleteService.new(@group, current_user, params)
+ end
+
+ def issuable_serializer
+ GroupIssuableAutocompleteSerializer.new
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def target
+ QuickActions::TargetService
+ .new(nil, current_user, group: @group)
+ .execute(params[:type], params[:type_id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
+
+Groups::AutocompleteSourcesController.prepend_mod
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index be38fe25842..e1f09d73739 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -5,7 +5,6 @@ class Groups::BoardsController < Groups::ApplicationController
include RecordUserLastActivity
include Gitlab::Utils::StrongMemoize
- before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
diff --git a/app/controllers/groups/email_campaigns_controller.rb b/app/controllers/groups/email_campaigns_controller.rb
index 4ce7d86be3c..c1e3ce519cc 100644
--- a/app/controllers/groups/email_campaigns_controller.rb
+++ b/app/controllers/groups/email_campaigns_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Groups::EmailCampaignsController < Groups::ApplicationController
- include InProductMarketingHelper
-
EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
feature_category :navigation
@@ -18,11 +16,13 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
def track_click
if Gitlab.com?
+ message = Gitlab::Email::Message::InProductMarketing.for(@track).new(group: group, series: @series)
+
data = {
namespace_id: group.id,
track: @track.to_s,
series: @series,
- subject_line: subject_line(@track, @series)
+ subject_line: message.subject_line
}
context = SnowplowTracker::SelfDescribingJson.new(EMAIL_CAMPAIGNS_SCHEMA_URL, data)
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 5df7ff0632a..c2ac56ccc63 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -4,6 +4,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include MembersPresentation
include SortingHelper
+ include Gitlab::Utils::StrongMemoize
MEMBER_PER_PAGE_LIMIT = 50
@@ -21,16 +22,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
feature_category :authentication_and_authorization
+ helper_method :can_manage_members?
+
def index
+ preload_max_access
@sort = params[:sort].presence || sort_value_name
- @project = @group.projects.find(params[:project_id]) if params[:project_id]
-
@members = GroupMembersFinder
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
- if can_manage_members
+ if can_manage_members?
@skip_groups = @group.related_group_ids
@invited_members = @members.invite
@@ -52,8 +54,18 @@ class Groups::GroupMembersController < Groups::ApplicationController
private
- def can_manage_members
- can?(current_user, :admin_group_member, @group)
+ def preload_max_access
+ return unless current_user
+
+ # this allows the can? against admin type queries in this action to
+ # only perform the query once, even if it is cached
+ current_user.max_access_for_group[@group.id] = @group.max_member_access(current_user)
+ end
+
+ def can_manage_members?
+ strong_memoize(:can_manage_members) do
+ can?(current_user, :admin_group_member, @group)
+ end
end
def present_invited_members(invited_members)
@@ -77,4 +89,4 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
-Groups::GroupMembersController.prepend_if_ee('EE::Groups::GroupMembersController')
+Groups::GroupMembersController.prepend_mod_with('Groups::GroupMembersController')
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 84dc570a1e9..e9dce3947dd 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -21,7 +21,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def new
- @milestone = Milestone.new
+ @noteable = @milestone = Milestone.new
end
def create
@@ -70,7 +70,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone
- @milestone = group.milestones.find_by_iid(params[:id])
+ @noteable = @milestone ||= group.milestones.find_by_iid(params[:id])
render_404 unless @milestone
end
@@ -95,4 +95,4 @@ class Groups::MilestonesController < Groups::ApplicationController
end
end
-Groups::MilestonesController.prepend_if_ee('EE::Groups::MilestonesController')
+Groups::MilestonesController.prepend_mod_with('Groups::MilestonesController')
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index dbfd31ebcad..b02b0e85d38 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -10,7 +10,6 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :continuous_integration
def show
- render 'shared/runners/show'
end
def edit
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index f1a6bcbe825..88c709e3f53 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -100,4 +100,4 @@ module Groups
end
end
-Groups::Settings::CiCdController.prepend_if_ee('EE::Groups::Settings::CiCdController')
+Groups::Settings::CiCdController.prepend_mod_with('Groups::Settings::CiCdController')
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index c3c93fe238a..8e3b2cb5d1b 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -12,11 +12,11 @@ module Groups
layout 'group_settings'
def index
- @integrations = Service.find_or_initialize_all_non_project_specific(Service.for_group(group)).sort_by(&:title)
+ @integrations = Integration.find_or_initialize_all_non_project_specific(Integration.for_group(group)).sort_by(&:title)
end
def edit
- @default_integration = Service.default_integration(integration.type, group)
+ @default_integration = Integration.default_integration(integration.type, group)
super
end
@@ -24,7 +24,7 @@ module Groups
private
def find_or_initialize_non_project_specific_integration(name)
- Service.find_or_initialize_non_project_specific_integration(name, group_id: group.id)
+ Integration.find_or_initialize_non_project_specific_integration(name, group_id: group.id)
end
def scoped_edit_integration_path(integration)
diff --git a/app/controllers/groups/settings/packages_and_registries_controller.rb b/app/controllers/groups/settings/packages_and_registries_controller.rb
index 90fb6497e20..c44e0727ff9 100644
--- a/app/controllers/groups/settings/packages_and_registries_controller.rb
+++ b/app/controllers/groups/settings/packages_and_registries_controller.rb
@@ -9,7 +9,7 @@ module Groups
feature_category :package_registry
- def index
+ def show
end
private
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 75bb6975c6e..00ddb8d736c 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -57,4 +57,4 @@ module Groups
end
end
-Groups::VariablesController.prepend_if_ee('EE::Groups::VariablesController')
+Groups::VariablesController.prepend_mod_with('Groups::VariablesController')
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 63f138aa462..a755d242d4a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -35,10 +35,6 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, @group)
end
- before_action do
- set_not_query_feature_flag(@group)
- end
-
before_action :export_rate_limit, only: [:export, :download_export]
helper_method :captcha_required?
@@ -53,10 +49,9 @@ class GroupsController < Groups::ApplicationController
feature_category :subgroups, [
:index, :new, :create, :show, :edit, :update,
- :destroy, :details, :transfer
+ :destroy, :details, :transfer, :activity
]
- feature_category :audit_events, [:activity]
feature_category :issue_tracking, [:issues, :issues_calendar, :preview_markdown]
feature_category :code_review, [:merge_requests, :unfoldered_environment_names]
feature_category :projects, [:projects]
@@ -197,7 +192,7 @@ class GroupsController < Groups::ApplicationController
def unfoldered_environment_names
respond_to do |format|
format.json do
- render json: EnvironmentNamesFinder.new(@group, current_user).execute
+ render json: Environments::EnvironmentNamesFinder.new(@group, current_user).execute
end
end
end
@@ -369,4 +364,4 @@ class GroupsController < Groups::ApplicationController
end
end
-GroupsController.prepend_if_ee('EE::GroupsController')
+GroupsController.prepend_mod_with('GroupsController')
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 87cda723895..1121ecfb65c 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -97,7 +97,7 @@ class Import::BaseController < ApplicationController
group = Groups::NestedCreateService.new(current_user, group_path: names).execute
group.errors.any? ? current_user.namespace : group
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error(e)
current_user.namespace
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 17f937a3dfd..9f91f3a1e1c 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -15,7 +15,7 @@ class Import::FogbugzController < Import::BaseController
def callback
begin
res = Gitlab::FogbugzImport::Client.new(import_params.to_h.symbolize_keys)
- rescue
+ rescue StandardError
# If the URI is invalid various errors can occur
return redirect_to new_import_fogbugz_path, alert: _('Could not connect to FogBugz, check your URL')
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index beb3e92b5ea..22bcd14d664 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -265,4 +265,4 @@ class Import::GithubController < Import::BaseController
end
end
-Import::GithubController.prepend_if_ee('EE::Import::GithubController')
+Import::GithubController.prepend_mod_with('Import::GithubController')
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 0eb08d2d0ad..0a9a9e03e94 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -3,10 +3,10 @@
class InvitesController < ApplicationController
include Gitlab::Utils::StrongMemoize
+ prepend_before_action :authenticate_user!, :track_invite_join_click, only: :show
before_action :member
before_action :ensure_member_exists
before_action :invite_details
- before_action :set_invite_type, only: :show
skip_before_action :authenticate_user!, only: :decline
helper_method :member?, :current_user_matches_invite?
@@ -16,18 +16,12 @@ class InvitesController < ApplicationController
feature_category :authentication_and_authorization
def show
- experiment('members/invite_email', actor: member).track(:opened) if initial_invite_email?
-
accept if skip_invitation_prompt?
end
def accept
if member.accept_invite!(current_user)
- experiment('members/invite_email', actor: member).track(:accepted) if initial_invite_email?
- session.delete(:invite_type)
-
- redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") %
- { member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] }
+ redirect_to invite_details[:path], notice: helpers.invite_accepted_notice(member)
else
redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
end
@@ -53,14 +47,6 @@ class InvitesController < ApplicationController
private
- def set_invite_type
- session[:invite_type] = params[:invite_type] if params[:invite_type].in?([Members::InviteEmailExperiment::INVITE_TYPE])
- end
-
- def initial_invite_email?
- session[:invite_type] == Members::InviteEmailExperiment::INVITE_TYPE
- end
-
def skip_invitation_prompt?
!member? && current_user_matches_invite?
end
@@ -85,21 +71,48 @@ class InvitesController < ApplicationController
def ensure_member_exists
return if member
- render_404
+ redirect_back_or_default(options: { alert: _("The invitation can not be found with the provided invite token.") })
+ end
+
+ def track_invite_join_click
+ experiment('members/invite_email', actor: member).track(:join_clicked) if member && Members::InviteEmailExperiment.initial_invite_email?(params[:invite_type])
end
def authenticate_user!
return if current_user
- store_location_for :user, request.fullpath
+ store_location_for(:user, invite_landing_url) if member
if user_sign_up?
- redirect_to new_user_registration_path(invite_email: member.invite_email), notice: _("To accept this invitation, create an account or sign in.")
+ set_session_invite_params
+
+ experiment(:invite_signup_page_interaction, actor: member) do |experiment_instance|
+ set_originating_member_id if experiment_instance.enabled?
+
+ experiment_instance.use do
+ redirect_to new_user_registration_path(invite_email: member.invite_email), notice: _("To accept this invitation, create an account or sign in.")
+ end
+ experiment_instance.try do
+ redirect_to new_users_sign_up_invite_path(invite_email: member.invite_email)
+ end
+
+ experiment_instance.track(:view)
+ end
else
redirect_to new_user_session_path(sign_in_redirect_params), notice: sign_in_notice
end
end
+ def set_session_invite_params
+ session[:invite_email] = member.invite_email
+
+ set_originating_member_id if Members::InviteEmailExperiment.initial_invite_email?(params[:invite_type])
+ end
+
+ def set_originating_member_id
+ session[:originating_member_id] = member.id
+ end
+
def sign_in_redirect_params
member ? { invite_email: member.invite_email } : {}
end
@@ -116,6 +129,10 @@ class InvitesController < ApplicationController
end
end
+ def invite_landing_url
+ root_url + invite_details[:path]
+ end
+
def invite_details
@invite_details ||= case member.source
when Project
@@ -123,14 +140,14 @@ class InvitesController < ApplicationController
name: member.source.full_name,
url: project_url(member.source),
title: _("project"),
- path: project_path(member.source)
+ path: member.source.activity_path
}
when Group
{
name: member.source.name,
url: group_url(member.source),
title: _("group"),
- path: group_path(member.source)
+ path: member.source.activity_path
}
end
end
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index 9c311f92b69..a6529ecb4ce 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -24,7 +24,7 @@ class JiraConnect::ApplicationController < ApplicationController
# Make sure `qsh` claim matches the current request
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
- rescue
+ rescue StandardError
render_403
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index ebc35448964..6aa46b8e4c3 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -38,4 +38,4 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
end
end
-Ldap::OmniauthCallbacksController.prepend_if_ee('EE::Ldap::OmniauthCallbacksController')
+Ldap::OmniauthCallbacksController.prepend_mod_with('Ldap::OmniauthCallbacksController')
diff --git a/app/controllers/oauth/jira/authorizations_controller.rb b/app/controllers/oauth/jira/authorizations_controller.rb
index f23149c8544..8169b5fcbb0 100644
--- a/app/controllers/oauth/jira/authorizations_controller.rb
+++ b/app/controllers/oauth/jira/authorizations_controller.rb
@@ -16,7 +16,7 @@ class Oauth::Jira::AuthorizationsController < ApplicationController
redirect_to oauth_authorization_path(client_id: params['client_id'],
response_type: 'code',
- scope: params['scope'],
+ scope: normalize_scope(params['scope']),
redirect_uri: oauth_jira_callback_url)
end
@@ -48,4 +48,12 @@ class Oauth::Jira::AuthorizationsController < ApplicationController
rescue Doorkeeper::Errors::DoorkeeperError => e
render status: :unauthorized, body: e.type
end
+
+ private
+
+ # When using the GitHub Enterprise connector in Jira we receive the "repo" scope,
+ # this doesn't exist in GitLab but we can map it to our "api" scope.
+ def normalize_scope(scope)
+ scope == 'repo' ? 'api' : scope
+ end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index af502c083d7..31f404a9974 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -289,4 +289,4 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
-OmniauthCallbacksController.prepend_if_ee('EE::OmniauthCallbacksController')
+OmniauthCallbacksController.prepend_mod_with('OmniauthCallbacksController')
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index bc6975f8953..2c0ed825daa 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -69,4 +69,4 @@ class PasswordsController < Devise::PasswordsController
end
end
-PasswordsController.prepend_if_ee('EE::PasswordsController')
+PasswordsController.prepend_mod_with('PasswordsController')
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index d8419be9f23..bd52ef0b0d4 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -37,4 +37,4 @@ class Profiles::AccountsController < Profiles::ApplicationController
end
end
-Profiles::AccountsController.prepend_if_ee('EE::Profiles::AccountsController')
+Profiles::AccountsController.prepend_mod_with('Profiles::AccountsController')
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 251967a7dff..ba539ef808d 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -60,4 +60,4 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
end
-Profiles::PersonalAccessTokensController.prepend_if_ee('EE::Profiles::PersonalAccessTokensController')
+Profiles::PersonalAccessTokensController.prepend_mod_with('Profiles::PersonalAccessTokensController')
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 45bab5f6cd1..adecb56ea38 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -55,4 +55,4 @@ class Profiles::PreferencesController < Profiles::ApplicationController
end
end
-Profiles::PreferencesController.prepend_if_ee('::EE::Profiles::PreferencesController')
+Profiles::PreferencesController.prepend_mod_with('Profiles::PreferencesController')
diff --git a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb
new file mode 100644
index 00000000000..7b4f6739a9b
--- /dev/null
+++ b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
+ respond_to :json
+
+ feature_category :planning_analytics
+
+ before_action :authorize_read_cycle_analytics!
+ before_action :only_default_value_stream_is_allowed!
+
+ def index
+ result = list_service.execute
+
+ if result.success?
+ render json: cycle_analytics_configuration(result.payload[:stages])
+ else
+ render json: { message: result.message }, status: result.http_status
+ end
+ end
+
+ private
+
+ def only_default_value_stream_is_allowed!
+ render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
+ end
+
+ def value_stream
+ Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
+ end
+
+ def list_params
+ { value_stream: value_stream }
+ end
+
+ def list_service
+ Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
+ end
+
+ def cycle_analytics_configuration(stages)
+ stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
+
+ Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
+ end
+end
diff --git a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
new file mode 100644
index 00000000000..03dcb164d94
--- /dev/null
+++ b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::ApplicationController
+ respond_to :json
+
+ feature_category :planning_analytics
+
+ before_action :authorize_read_cycle_analytics!
+
+ def index
+ # FOSS users can only see the default value stream
+ value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)]
+
+ render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
+ end
+end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 001967b8bb4..7c419cac1cc 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -49,4 +49,4 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
end
-Projects::AutocompleteSourcesController.prepend_if_ee('EE::Projects::AutocompleteSourcesController')
+Projects::AutocompleteSourcesController.prepend_mod_with('Projects::AutocompleteSourcesController')
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 2c7c49b4250..1df7b9ed165 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -20,7 +20,7 @@ class Projects::BlameController < Projects::ApplicationController
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
- @environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
+ @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@blame = Gitlab::Blame.new(@blob, @commit)
@blame = Gitlab::View::Presenter::Factory.new(@blame, project: @project, path: @path).fabricate!
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index a398fc56a35..dbe628cb43a 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -31,19 +31,23 @@ class Projects::BlobController < Projects::ApplicationController
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update]
- before_action :record_experiment, only: :new
+ before_action :track_experiment, only: :create
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
feature_category :source_code_management
+ before_action do
+ push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
+ end
+
def new
commit unless @repository.empty?
end
def create
create_commit(Files::CreateService, success_notice: _("The file has been successfully created."),
- success_path: -> { project_blob_path(@project, File.join(@branch_name, @file_path)) },
+ success_path: -> { create_success_path },
failure_view: :new,
failure_path: project_new_blob_path(@project, @ref))
end
@@ -214,7 +218,7 @@ class Projects::BlobController < Projects::ApplicationController
def show_html
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
environment_params[:find_latest] = true
- @environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
+ @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last
@last_commit = @repository.last_commit_for_path(@commit.id, @blob.path, literal_pathspec: true)
@code_navigation_path = Gitlab::CodeNavigationPath.new(@project, @blob.commit_id).full_json_path_for(@blob.path)
@@ -262,9 +266,17 @@ class Projects::BlobController < Projects::ApplicationController
current_user&.id
end
- def record_experiment
- return unless params[:file_name] == @project.ci_config_path_or_default && @project.namespace.recent?
+ def create_success_path
+ if params[:code_quality_walkthrough]
+ project_pipelines_path(@project, code_quality_walkthrough: true)
+ else
+ project_blob_path(@project, File.join(@branch_name, @file_path))
+ end
+ end
+
+ def track_experiment
+ return unless params[:code_quality_walkthrough]
- record_experiment_user(:ci_syntax_templates_b, namespace_id: @project.namespace_id)
+ experiment(:code_quality_walkthrough, namespace: @project.root_ancestor).track(:commit_created)
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 349649c7b35..9a3e9437426 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -5,7 +5,6 @@ class Projects::BoardsController < Projects::ApplicationController
include IssuableCollections
before_action :check_issues_available!
- before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 754e2ccf4f9..6e31816bc99 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -3,11 +3,9 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
- push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_branch_switcher, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml)
end
feature_category :pipeline_authoring
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 0c3ff07bc76..863715429ff 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -24,7 +24,7 @@ class Projects::CommitController < Projects::ApplicationController
end
BRANCH_SEARCH_LIMIT = 1000
- COMMIT_DIFFS_PER_PAGE = 75
+ COMMIT_DIFFS_PER_PAGE = 20
feature_category :source_code_management
@@ -49,7 +49,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def diff_files
- render json: { html: view_to_html_string('projects/commit/diff_files', diffs: @diffs, environment: @environment) }
+ render template: 'projects/commit/diff_files', layout: false, locals: { diffs: @diffs, environment: @environment }
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -167,7 +167,7 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
- @environment = EnvironmentsByDeploymentsFinder.new(@project, current_user, commit: @commit, find_latest: true).execute.last
+ @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, commit: @commit, find_latest: true).execute.last
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 221bc16e256..28a87f83451 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -26,6 +26,10 @@ class Projects::CompareController < Projects::ApplicationController
feature_category :source_code_management
+ # Diffs may be pretty chunky, the less is better in this endpoint.
+ # Pagination design guides: https://design.gitlab.com/components/pagination/#behavior
+ COMMIT_DIFFS_PER_PAGE = 20
+
def index
end
@@ -132,7 +136,7 @@ class Projects::CompareController < Projects::ApplicationController
if compare
environment_params = source_project.repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
environment_params[:find_latest] = true
- @environment = EnvironmentsByDeploymentsFinder.new(source_project, current_user, environment_params).execute.last
+ @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user, environment_params).execute.last
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 92483607e65..76de9a83c87 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -264,4 +264,4 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
-Projects::EnvironmentsController.prepend_if_ee('EE::Projects::EnvironmentsController')
+Projects::EnvironmentsController.prepend_mod_with('Projects::EnvironmentsController')
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 9fc8e8c063b..8fa3635a737 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -131,4 +131,4 @@ class Projects::ForksController < Projects::ApplicationController
end
end
-Projects::ForksController.prepend_if_ee('EE::Projects::ForksController')
+Projects::ForksController.prepend_mod_with('Projects::ForksController')
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index c6c90ffaba2..27893fe510d 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -60,4 +60,4 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
end
-Projects::GroupLinksController.prepend_if_ee('EE::Projects::GroupLinksController')
+Projects::GroupLinksController.prepend_mod_with('Projects::GroupLinksController')
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 8dabf3e640b..b87bfc58f8b 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -32,6 +32,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def edit
+ redirect_to(action: :index) unless hook
end
def update
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index c8528ad6d28..3b3f9bdcf6b 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -81,4 +81,4 @@ class Projects::ImportsController < Projects::ApplicationController
end
end
-Projects::ImportsController.prepend_if_ee('EE::Projects::ImportsController')
+Projects::ImportsController.prepend_mod_with('Projects::ImportsController')
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cae5cc411bc..01a6de76ba5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -56,8 +56,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
- record_experiment_user(:invite_members_version_b)
-
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
@@ -110,12 +108,12 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_ids: ""
)
- build_params = issue_create_params.merge(
+ build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve],
- confidential: !!Gitlab::Utils.to_boolean(issue_create_params[:confidential])
+ confidential: !!Gitlab::Utils.to_boolean(issue_params[:confidential])
)
- service = ::Issues::BuildService.new(project, current_user, build_params)
+ service = ::Issues::BuildService.new(project: project, current_user: current_user, params: build_params)
@issue = @noteable = service.execute
@@ -130,12 +128,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- create_params = issue_create_params.merge(spammable_params).merge(
+ create_params = issue_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
- service = ::Issues::CreateService.new(project, current_user, create_params)
+ service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params)
@issue = service.execute
create_vulnerability_issue_feedback(issue)
@@ -162,7 +160,7 @@ class Projects::IssuesController < Projects::ApplicationController
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
- @issue = ::Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
+ @issue = ::Issues::UpdateService.new(project: project, current_user: current_user, params: { target_project: new_project }).execute(issue)
end
respond_to do |format|
@@ -176,7 +174,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def reorder
- service = ::Issues::ReorderService.new(project, current_user, reorder_params)
+ service = ::Issues::ReorderService.new(project: project, current_user: current_user, params: reorder_params)
if service.execute(issue)
head :ok
@@ -187,7 +185,7 @@ class Projects::IssuesController < Projects::ApplicationController
def related_branches
@related_branches = ::Issues::RelatedBranchesService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.execute(issue)
.map { |branch| branch.merge(link: branch_link(branch)) }
@@ -215,7 +213,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create_merge_request
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
create_params[:target_project_id] = params[:target_project_id]
- result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
+ result = ::MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_params).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
@@ -316,17 +314,8 @@ class Projects::IssuesController < Projects::ApplicationController
task_num
lock_version
discussion_locked
- ] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
- end
-
- def issue_create_params
- create_params = %i[
issue_type
- ]
-
- params.require(:issue).permit(
- *create_params
- ).merge(issue_params)
+ ] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
end
def reorder_params
@@ -345,7 +334,7 @@ class Projects::IssuesController < Projects::ApplicationController
def update_service
update_params = issue_params.merge(spammable_params)
- ::Issues::UpdateService.new(project, current_user, update_params)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: update_params)
end
def finder_type
@@ -402,4 +391,4 @@ class Projects::IssuesController < Projects::ApplicationController
def create_vulnerability_issue_feedback(issue); end
end
-Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')
+Projects::IssuesController.prepend_mod_with('Projects::IssuesController')
diff --git a/app/controllers/projects/logs_controller.rb b/app/controllers/projects/logs_controller.rb
index f9b8091a419..a4bdbc827e0 100644
--- a/app/controllers/projects/logs_controller.rb
+++ b/app/controllers/projects/logs_controller.rb
@@ -58,7 +58,7 @@ module Projects
def environment
strong_memoize(:environment) do
if cluster_params.key?(:environment_name)
- EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).execute.first
+ ::Environments::EnvironmentsFinder.new(project, current_user, name: cluster_params[:environment_name]).execute.first
else
project.default_environment
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index e74717a44ab..78170fab7a7 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -65,4 +65,4 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
end
-Projects::MergeRequests::ApplicationController.prepend_if_ee('EE::Projects::MergeRequests::ApplicationController')
+Projects::MergeRequests::ApplicationController.prepend_mod_with('Projects::MergeRequests::ApplicationController')
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index dc77b5e09c8..9f1e2d8236a 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -19,7 +19,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end
def create
- @merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
+ @merge_request = ::MergeRequests::CreateService.new(project: project, current_user: current_user, params: merge_request_params).execute
if @merge_request.valid?
incr_count_webide_merge_request
@@ -93,7 +93,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
# Gitaly N+1 issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/58096
Gitlab::GitalyClient.allow_n_plus_1_calls do
- @merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
+ @merge_request = ::MergeRequests::BuildService.new(project: project, current_user: current_user, params: merge_request_params.merge(diff_options: diff_options)).execute
end
end
@@ -141,4 +141,4 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end
end
-Projects::MergeRequests::CreationsController.prepend_ee_mod
+Projects::MergeRequests::CreationsController.prepend_mod
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 98ef9d918ae..3eaabfbf33e 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -47,7 +47,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
diffs = @compare.diffs(diff_options)
render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user)
- .represent(diffs, additional_attributes)
+ .represent(diffs, additional_attributes.merge(only_context_commits: show_only_context_commits?))
end
private
@@ -92,7 +92,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
# rubocop: disable CodeReuse/ActiveRecord
def commit
return unless commit_id = params[:commit_id].presence
- return unless @merge_request.all_commits.exists?(sha: commit_id)
+ return unless @merge_request.all_commits.exists?(sha: commit_id) || @merge_request.recent_context_commits.map(&:id).include?(commit_id)
@commit ||= @project.commit(commit_id)
end
@@ -122,6 +122,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
+ return @merge_request.context_commits_diff if show_only_context_commits? && !@merge_request.context_commits_diff.empty?
return @merge_request.merge_head_diff if render_merge_ref_head_diff?
if @start_sha
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 4e409b5f28f..613faa200d1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -42,11 +42,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:usage_data_i_testing_summary_widget_total, @project, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
-
- record_experiment_user(:invite_members_version_b)
+ push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_import_members?
@@ -60,6 +60,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
+ push_frontend_feature_flag(:show_relevant_approval_rule_approvers, @project, default_enabled: :yaml)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
@@ -114,6 +115,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count + @merge_request.context_commits_count
+ @diffs_count = get_diffs_count
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
@current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestCurrentUserEntity).to_json
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
@@ -244,7 +246,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def update
- @merge_request = ::MergeRequests::UpdateService.new(project, current_user, merge_request_update_params).execute(@merge_request)
+ @merge_request = ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: merge_request_update_params).execute(@merge_request)
respond_to do |format|
format.html do
@@ -273,7 +275,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def remove_wip
@merge_request = ::MergeRequests::UpdateService
- .new(project, current_user, wip_event: 'unwip')
+ .new(project: project, current_user: current_user, params: { wip_event: 'unwip' })
.execute(@merge_request)
render json: serialize_widget(@merge_request)
@@ -308,7 +310,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def assign_related_issues
- result = ::MergeRequests::AssignIssuesService.new(project, current_user, merge_request: @merge_request).execute
+ result = ::MergeRequests::AssignIssuesService.new(project: project, current_user: current_user, params: { merge_request: @merge_request }).execute
case result[:count]
when 0
@@ -386,6 +388,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
+ def get_diffs_count
+ if show_only_context_commits?
+ @merge_request.context_commits_diff.raw_diffs.size
+ else
+ @merge_request.diff_size
+ end
+ end
+
def merge_request_update_params
merge_request_params.merge!(params.permit(:merge_request_diff_head_sha))
end
@@ -412,7 +422,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :failed
end
- merge_service = ::MergeRequests::MergeService.new(@project, current_user, merge_params)
+ merge_service = ::MergeRequests::MergeService.new(project: @project, current_user: current_user, params: merge_params)
unless merge_service.hooks_validation_pass?(@merge_request)
return :hook_validation_error
@@ -525,4 +535,4 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
-Projects::MergeRequestsController.prepend_if_ee('EE::Projects::MergeRequestsController')
+Projects::MergeRequestsController.prepend_mod_with('Projects::MergeRequestsController')
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index dcd3c49441e..dcdda18784d 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -39,7 +39,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def new
- @milestone = @project.milestones.new
+ @noteable = @milestone = @project.milestones.new
respond_with(@milestone)
end
@@ -125,7 +125,7 @@ class Projects::MilestonesController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def milestone
- @milestone ||= @project.milestones.find_by!(iid: params[:id])
+ @noteable = @milestone ||= @project.milestones.find_by!(iid: params[:id])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 01abb72fc86..bcb6b574d5a 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -94,4 +94,4 @@ class Projects::MirrorsController < Projects::ApplicationController
end
end
-Projects::MirrorsController.prepend_if_ee('EE::Projects::MirrorsController')
+Projects::MirrorsController.prepend_mod_with('Projects::MirrorsController')
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 0aac517e3e3..4bd33882eee 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -55,4 +55,4 @@ class Projects::PagesController < Projects::ApplicationController
end
end
-Projects::PagesController.prepend_if_ee('EE::Projects::PagesController')
+Projects::PagesController.prepend_mod_with('Projects::PagesController')
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 9f326ef59f5..0de8dc597ae 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -13,14 +13,12 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
- push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_graph_layers_view, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_filter_jobs, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
- push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
end
- before_action :ensure_pipeline, only: [:show]
+ before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
@@ -33,7 +31,12 @@ class Projects::PipelinesController < Projects::ApplicationController
POLLING_INTERVAL = 10_000
- feature_category :continuous_integration
+ feature_category :continuous_integration, [
+ :charts, :show, :config_variables, :stage, :cancel, :retry,
+ :builds, :dag, :failures, :status, :downloadable_artifacts,
+ :index, :create, :new, :destroy
+ ]
+ feature_category :code_testing, [:test_report]
def index
@pipelines = Ci::PipelinesFinder
@@ -55,6 +58,17 @@ class Projects::PipelinesController < Projects::ApplicationController
e.try {}
e.track(:view, value: project.namespace_id)
end
+ experiment(:code_quality_walkthrough, namespace: project.root_ancestor) do |e|
+ e.exclude! unless current_user
+ e.exclude! unless can?(current_user, :create_pipeline, project)
+ e.exclude! unless project.root_ancestor.recent?
+ e.exclude! if @pipelines_count.to_i > 0
+ e.exclude! if helpers.has_gitlab_ci?(project)
+
+ e.use {}
+ e.try {}
+ e.track(:view, property: project.root_ancestor.id.to_s)
+ end
end
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
@@ -162,7 +176,11 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def retry
- pipeline.retry_failed(current_user)
+ if Gitlab::Ci::Features.background_pipeline_retry_endpoint?(@project)
+ ::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
+ else
+ pipeline.retry_failed(current_user)
+ end
respond_to do |format|
format.html do
@@ -206,13 +224,20 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def downloadable_artifacts
+ render json: Ci::DownloadableArtifactSerializer.new(
+ project: project,
+ current_user: current_user
+ ).represent(@pipeline)
+ end
+
private
def serialize_pipelines
PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines, disable_coverage: true, preload: true)
+ .represent(@pipelines, disable_coverage: true, preload: true, code_quality_walkthrough: params[:code_quality_walkthrough].present?)
end
def render_show
@@ -298,4 +323,4 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
-Projects::PipelinesController.prepend_if_ee('EE::Projects::PipelinesController')
+Projects::PipelinesController.prepend_mod_with('Projects::PipelinesController')
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 5972b29a298..cc2157a7d51 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -64,4 +64,4 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
-Projects::ProjectMembersController.prepend_if_ee('EE::Projects::ProjectMembersController')
+Projects::ProjectMembersController.prepend_mod_with('Projects::ProjectMembersController')
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 84b155c8002..8c70ef446a2 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -29,4 +29,4 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
end
end
-Projects::ProtectedBranchesController.prepend_if_ee('EE::Projects::ProtectedBranchesController')
+Projects::ProtectedBranchesController.prepend_mod_with('Projects::ProtectedBranchesController')
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index 4cba1a75330..abbfe9ce22a 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -68,4 +68,4 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
end
-Projects::ProtectedRefsController.prepend_if_ee('EE::Projects::ProtectedRefsController')
+Projects::ProtectedRefsController.prepend_mod_with('Projects::ProtectedRefsController')
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 26382856761..1bb50eabd1d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -8,11 +8,6 @@ class Projects::ReleasesController < Projects::ApplicationController
# We have to check `download_code` permission because detail URL path
# contains git-tag name.
before_action :authorize_download_code!, except: [:index]
- before_action do
- push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true)
- push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true)
- push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true)
- end
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index da018b24836..8f64a8aa1d3 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -35,7 +35,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
return if archive_not_modified?
send_git_archive @repository, **repo_params
- rescue => ex
+ rescue StandardError => ex
logger.error("#{self.class.name}: #{ex}")
git_not_found!
end
@@ -127,4 +127,4 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
end
-Projects::RepositoriesController.prepend_if_ee('EE::Projects::RepositoriesController')
+Projects::RepositoriesController.prepend_mod_with('Projects::RepositoriesController')
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index d225d5e104c..fa6adc9431d 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -17,7 +17,10 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
if @runner.assign_to(project, current_user)
redirect_to path
else
- redirect_to path, alert: 'Failed adding runner to project'
+ assign_to_messages = @runner.errors.messages[:assign_to]
+ alert = assign_to_messages&.join(',') || 'Failed adding runner to project'
+
+ redirect_to path, alert: alert
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index b7a5a63e642..ec1f57f090a 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -48,28 +48,17 @@ class Projects::RunnersController < Projects::ApplicationController
end
def show
- render 'shared/runners/show'
end
def toggle_shared_runners
if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
-
- if Feature.enabled?(:vueify_shared_runners_toggle, @project)
- render json: { error: _('Cannot enable shared runners because parent group does not allow it') }, status: :unauthorized
- else
- redirect_to project_runners_path(@project), alert: _('Cannot enable shared runners because parent group does not allow it')
- end
-
+ render json: { error: _('Cannot enable shared runners because parent group does not allow it') }, status: :unauthorized
return
end
project.toggle!(:shared_runners_enabled)
- if Feature.enabled?(:vueify_shared_runners_toggle, @project)
- render json: {}, status: :ok
- else
- redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings')
- end
+ render json: {}, status: :ok
end
def toggle_group_runners
diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb
index bc4e58e54a9..19de157357a 100644
--- a/app/controllers/projects/security/configuration_controller.rb
+++ b/app/controllers/projects/security/configuration_controller.rb
@@ -14,4 +14,4 @@ module Projects
end
end
-Projects::Security::ConfigurationController.prepend_if_ee('EE::Projects::Security::ConfigurationController')
+Projects::Security::ConfigurationController.prepend_mod_with('Projects::Security::ConfigurationController')
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index ccb8b393bfe..74145a70b95 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -1,19 +1,16 @@
# frozen_string_literal: true
class Projects::ServicesController < Projects::ApplicationController
- include ServiceParams
+ include Integrations::Params
include InternalRedirect
# Authorize
before_action :authorize_admin_project!
before_action :ensure_service_enabled
- before_action :service
+ before_action :integration
before_action :web_hook_logs, only: [:edit, :update]
before_action :set_deprecation_notice_for_prometheus_service, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_service, only: [:update]
- before_action only: :edit do
- push_frontend_feature_flag(:jira_for_vulnerabilities, @project, type: :development, default_enabled: :yaml)
- end
respond_to :html
@@ -22,20 +19,19 @@ class Projects::ServicesController < Projects::ApplicationController
feature_category :integrations
def edit
- @default_integration = Service.default_integration(service.type, project)
+ @default_integration = Integration.default_integration(service.type, project)
end
def update
- @service.attributes = service_params[:service]
- @service.inherit_from_id = nil if service_params[:service][:inherit_from_id].blank?
+ @integration.attributes = integration_params[:integration]
+ @integration.inherit_from_id = nil if integration_params[:integration][:inherit_from_id].blank?
- saved = @service.save(context: :manual_change)
+ saved = @integration.save(context: :manual_change)
respond_to do |format|
format.html do
if saved
- target_url = safe_redirect_path(params[:redirect_to]).presence || edit_project_service_path(@project, @service)
- redirect_to target_url, notice: success_message
+ redirect_to redirect_path, notice: success_message
else
render 'edit'
end
@@ -50,7 +46,7 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
- if @service.can_test?
+ if integration.can_test?
render json: service_test_response, status: :ok
else
render json: {}, status: :not_found
@@ -59,12 +55,16 @@ class Projects::ServicesController < Projects::ApplicationController
private
+ def redirect_path
+ safe_redirect_path(params[:redirect_to]).presence || edit_project_service_path(@project, @integration)
+ end
+
def service_test_response
- unless @service.update(service_params[:service])
- return { error: true, message: _('Validations failed.'), service_response: @service.errors.full_messages.join(','), test_failed: false }
+ unless @integration.update(integration_params[:integration])
+ return { error: true, message: _('Validations failed.'), service_response: @integration.errors.full_messages.join(','), test_failed: false }
end
- result = ::Integrations::Test::ProjectService.new(@service, current_user, params[:event]).execute
+ result = ::Integrations::Test::ProjectService.new(@integration, current_user, params[:event]).execute
unless result[:success]
return { error: true, message: s_('Integrations|Connection failed. Please check your settings.'), service_response: result[:message].to_s, test_failed: true }
@@ -76,16 +76,18 @@ class Projects::ServicesController < Projects::ApplicationController
end
def success_message
- if @service.active?
- s_('Integrations|%{integration} settings saved and active.') % { integration: @service.title }
+ if integration.active?
+ s_('Integrations|%{integration} settings saved and active.') % { integration: integration.title }
else
- s_('Integrations|%{integration} settings saved, but not active.') % { integration: @service.title }
+ s_('Integrations|%{integration} settings saved, but not active.') % { integration: integration.title }
end
end
- def service
- @service ||= @project.find_or_initialize_service(params[:id])
+ def integration
+ @integration ||= @project.find_or_initialize_service(params[:id])
+ @service ||= @integration # TODO: remove references to @service
end
+ alias_method :service, :integration
def web_hook_logs
return unless @service.service_hook.present?
@@ -98,17 +100,17 @@ class Projects::ServicesController < Projects::ApplicationController
end
def serialize_as_json
- @service
+ integration
.as_json(only: @service.json_fields)
.merge(errors: @service.errors.as_json)
end
def redirect_deprecated_prometheus_service
- redirect_to edit_project_service_path(project, @service) if @service.is_a?(::PrometheusService) && Feature.enabled?(:settings_operations_prometheus_service, project)
+ redirect_to edit_project_service_path(project, integration) if integration.is_a?(::PrometheusService) && Feature.enabled?(:settings_operations_prometheus_service, project)
end
def set_deprecation_notice_for_prometheus_service
- return if !@service.is_a?(::PrometheusService) || !Feature.enabled?(:settings_operations_prometheus_service, project)
+ return if !integration.is_a?(::PrometheusService) || !Feature.enabled?(:settings_operations_prometheus_service, project)
operations_link_start = "<a href=\"#{project_settings_operations_path(project)}\">"
message = s_('PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated.') % { operations_link_start: operations_link_start, operations_link_end: "</a>" }
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 1a465406660..3254d4129d3 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -12,7 +12,6 @@ module Projects
before_action :define_variables
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
- push_frontend_feature_flag(:vueify_shared_runners_toggle, @project)
end
helper_method :highlight_badge
@@ -119,12 +118,13 @@ module Projects
.assignable_for(project)
.ordered
.page(params[:specific_page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
+ .with_tags
- @shared_runners = ::Ci::Runner.instance_type.active
+ @shared_runners = ::Ci::Runner.instance_type.active.with_tags
@shared_runners_count = @shared_runners.count(:all)
- @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
+ @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id).with_tags
end
def define_ci_variables
@@ -143,7 +143,7 @@ module Projects
end
def define_badges_variables
- @ref = params[:ref] || @project.default_branch || 'master'
+ @ref = params[:ref] || @project.default_branch_or_main
@badges = [Gitlab::Ci::Badge::Pipeline::Status,
Gitlab::Ci::Badge::Coverage::Report]
@@ -160,4 +160,4 @@ module Projects
end
end
-Projects::Settings::CiCdController.prepend_if_ee('EE::Projects::Settings::CiCdController')
+Projects::Settings::CiCdController.prepend_mod_with('Projects::Settings::CiCdController')
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index a05793a0283..a357227c870 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -155,4 +155,4 @@ module Projects
end
end
-Projects::Settings::OperationsController.prepend_if_ee('::EE::Projects::Settings::OperationsController')
+Projects::Settings::OperationsController.prepend_mod_with('Projects::Settings::OperationsController')
diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb
new file mode 100644
index 00000000000..fee51dc1311
--- /dev/null
+++ b/app/controllers/projects/settings/packages_and_registries_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class PackagesAndRegistriesController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_admin_project!
+ before_action :packages_and_registries_settings_enabled!
+
+ feature_category :package_registry
+
+ def show
+ end
+
+ private
+
+ def packages_and_registries_settings_enabled!
+ render_404 unless settings_packages_and_registries_enabled?(project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index bb5ad8e9aea..728231dbdbd 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -134,4 +134,4 @@ module Projects
end
end
-Projects::Settings::RepositoryController.prepend_if_ee('EE::Projects::Settings::RepositoryController')
+Projects::Settings::RepositoryController.prepend_mod_with('Projects::Settings::RepositoryController')
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index ff28c3be298..de2ab16b5b1 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -13,6 +13,10 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
before_action :authorize_read_snippet!, except: [:new, :index]
before_action :authorize_update_snippet!, only: :edit
+ before_action only: [:show] do
+ push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml)
+ end
+
def index
@snippet_counts = ::Snippets::CountService
.new(current_user, project: @project)
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 3bf9988ca22..94b0473e1f3 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -9,9 +9,6 @@ class Projects::TagsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_admin_tag!, only: [:new, :create, :destroy]
- before_action do
- push_frontend_feature_flag(:gldropdown_tags, default_enabled: :yaml)
- end
feature_category :source_code_management, [:index, :show, :new, :destroy]
feature_category :release_evidence, [:create]
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index d1486f765e4..a1493a25a1a 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -6,4 +6,8 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project
feature_category :wiki
+
+ before_action do
+ push_frontend_feature_flag(:wiki_content_editor, project, default_enabled: :yaml)
+ end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 7c9d6daad02..e66893ac269 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -43,13 +43,12 @@ class ProjectsController < Projects::ApplicationController
feature_category :projects, [
:index, :show, :new, :create, :edit, :update, :transfer,
- :destroy, :resolve, :archive, :unarchive, :toggle_star
+ :destroy, :resolve, :archive, :unarchive, :toggle_star, :activity
]
feature_category :source_code_management, [:remove_fork, :housekeeping, :refs]
feature_category :issue_tracking, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
- feature_category :audit_events, [:activity]
feature_category :code_review, [:unfoldered_environment_names]
def index
@@ -85,7 +84,7 @@ class ProjectsController < Projects::ApplicationController
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
- render 'new', locals: { active_tab: active_new_project_tab }
+ render 'new'
end
end
@@ -311,7 +310,7 @@ class ProjectsController < Projects::ApplicationController
def unfoldered_environment_names
respond_to do |format|
format.json do
- render json: EnvironmentNamesFinder.new(@project, current_user).execute
+ render json: Environments::EnvironmentNamesFinder.new(@project, current_user).execute
end
end
end
@@ -545,4 +544,4 @@ class ProjectsController < Projects::ApplicationController
end
end
-ProjectsController.prepend_if_ee('EE::ProjectsController')
+ProjectsController.prepend_mod_with('ProjectsController')
diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb
index 3a721823d89..d04e8d296ed 100644
--- a/app/controllers/registrations/experience_levels_controller.rb
+++ b/app/controllers/registrations/experience_levels_controller.rb
@@ -38,7 +38,7 @@ module Registrations
end
def learn_gitlab
- @learn_gitlab ||= LearnGitlab.new(current_user)
+ @learn_gitlab ||= LearnGitlab::Project.new(current_user)
end
end
end
diff --git a/app/controllers/registrations/invites_controller.rb b/app/controllers/registrations/invites_controller.rb
new file mode 100644
index 00000000000..548714e80e9
--- /dev/null
+++ b/app/controllers/registrations/invites_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Registrations
+ class InvitesController < RegistrationsController
+ layout 'simple_registration'
+
+ before_action :check_if_gl_com_or_dev
+ end
+end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 62ec03206c4..87465f8714d 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -18,7 +18,13 @@ module Registrations
if result[:status] == :success
return redirect_to new_users_sign_up_group_path if show_signup_onboarding?
- redirect_to path_for_signed_in_user(current_user)
+ members = current_user.members
+
+ if members.count == 1 && members.last.source.present?
+ redirect_to members_activity_path(members), notice: helpers.invite_accepted_notice(members.last)
+ else
+ redirect_to path_for_signed_in_user(current_user)
+ end
else
render :show
end
@@ -48,7 +54,14 @@ module Registrations
def path_for_signed_in_user(user)
return users_almost_there_path if requires_confirmation?(user)
- stored_location_for(user) || dashboard_projects_path
+ stored_location_for(user) || members_activity_path(user.members)
+ end
+
+ def members_activity_path(members)
+ return dashboard_projects_path unless members.any?
+ return dashboard_projects_path unless members.last.source.present?
+
+ members.last.source.activity_path
end
def show_signup_onboarding?
@@ -57,4 +70,4 @@ module Registrations
end
end
-Registrations::WelcomeController.prepend_if_ee('EE::Registrations::WelcomeController')
+Registrations::WelcomeController.prepend_mod_with('Registrations::WelcomeController')
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 61218a95add..0f29f6f608f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -155,13 +155,21 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
- @resource ||= Users::BuildService.new(current_user, sign_up_params).execute
+ @resource ||= Users::RegistrationsBuildService
+ .new(current_user, sign_up_params.merge({ skip_confirmation: skip_email_confirmation? }))
+ .execute
end
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
+ def skip_email_confirmation?
+ invite_email = session.delete(:invite_email)
+
+ sign_up_params[:email] == invite_email
+ end
+
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
@@ -179,6 +187,21 @@ class RegistrationsController < Devise::RegistrationsController
def set_invite_params
@invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
end
+
+ def after_pending_invitations_hook
+ member_id = session.delete(:originating_member_id)
+
+ return unless member_id
+
+ # if invited multiple times to different projects, only the email clicked will be counted as accepted
+ # for the specific member on a project or group
+ member = resource.members.find_by(id: member_id) # rubocop: disable CodeReuse/ActiveRecord
+
+ return unless member
+
+ experiment(:invite_signup_page_interaction, actor: member).track(:form_submission)
+ experiment('members/invite_email', actor: member).track(:accepted)
+ end
end
-RegistrationsController.prepend_if_ee('EE::RegistrationsController')
+RegistrationsController.prepend_mod_with('RegistrationsController')
diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb
index a5b81054ee4..76d9983d341 100644
--- a/app/controllers/repositories/git_http_client_controller.rb
+++ b/app/controllers/repositories/git_http_client_controller.rb
@@ -134,4 +134,4 @@ module Repositories
end
end
-Repositories::GitHttpClientController.prepend_if_ee('EE::Repositories::GitHttpClientController')
+Repositories::GitHttpClientController.prepend_mod_with('Repositories::GitHttpClientController')
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index d68ba80ab5d..11a219b4ff0 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -122,4 +122,4 @@ module Repositories
end
end
-Repositories::GitHttpController.prepend_if_ee('EE::Repositories::GitHttpController')
+Repositories::GitHttpController.prepend_mod_with('Repositories::GitHttpController')
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index 2de29da4b45..4f2e02c78c3 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -148,4 +148,4 @@ module Repositories
end
end
-Repositories::LfsApiController.prepend_if_ee('EE::Repositories::LfsApiController')
+Repositories::LfsApiController.prepend_mod_with('Repositories::LfsApiController')
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 672a03ad11d..97b6671a82a 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -70,4 +70,4 @@ class RootController < Dashboard::ProjectsController
end
end
-RootController.prepend_if_ee('EE::RootController')
+RootController.prepend_mod_with('RootController')
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 3b218822395..ac6239615b4 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -152,4 +152,4 @@ class SearchController < ApplicationController
end
end
-SearchController.prepend_if_ee('EE::SearchController')
+SearchController.prepend_mod_with('SearchController')
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index db07b212d00..64d66ee86f1 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -53,4 +53,4 @@ class SentNotificationsController < ApplicationController
end
end
-SentNotificationsController.prepend_if_ee('EE::SentNotificationsController')
+SentNotificationsController.prepend_mod_with('SentNotificationsController')
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index b8842b2efdb..4fcf82c605b 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -22,6 +22,7 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
+ prepend_before_action :check_forbidden_password_based_login, if: -> { action_name == 'create' && password_based_login? }
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
before_action :auto_sign_in_with_provider, only: [:new]
@@ -313,6 +314,13 @@ class SessionsController < Devise::SessionsController
def set_invite_params
@invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
end
+
+ def check_forbidden_password_based_login
+ if find_user&.password_based_login_forbidden?
+ flash[:alert] = _('You are not allowed to log in using password')
+ redirect_to new_user_session_path
+ end
+ end
end
-SessionsController.prepend_if_ee('EE::SessionsController')
+SessionsController.prepend_mod_with('SessionsController')
diff --git a/app/controllers/terraform/services_controller.rb b/app/controllers/terraform/services_controller.rb
new file mode 100644
index 00000000000..e7b9a94fd8e
--- /dev/null
+++ b/app/controllers/terraform/services_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Terraform::ServicesController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ feature_category :infrastructure_as_code
+
+ def index
+ render json: { 'modules.v1' => "/api/#{::API::API.version}/packages/terraform/modules/v1/" }
+ end
+end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 2c827292928..4077a3d3dac 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -117,4 +117,4 @@ class UploadsController < ApplicationController
end
end
-UploadsController.prepend_if_ee('EE::UploadsController')
+UploadsController.prepend_mod_with('UploadsController')
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 54d97f588fc..287ee2d5ab8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -260,4 +260,4 @@ class UsersController < ApplicationController
end
end
-UsersController.prepend_if_ee('EE::UsersController')
+UsersController.prepend_mod_with('UsersController')
diff --git a/app/controllers/whats_new_controller.rb b/app/controllers/whats_new_controller.rb
index e24b0bbc7bb..6f389aa4924 100644
--- a/app/controllers/whats_new_controller.rb
+++ b/app/controllers/whats_new_controller.rb
@@ -5,6 +5,7 @@ class WhatsNewController < ApplicationController
skip_before_action :authenticate_user!
+ before_action :check_whats_new_enabled
before_action :check_valid_page_param, :set_pagination_headers
feature_category :navigation
@@ -19,6 +20,10 @@ class WhatsNewController < ApplicationController
private
+ def check_whats_new_enabled
+ render_404 if Gitlab::CurrentSettings.current_application_settings.whats_new_variant_disabled?
+ end
+
def check_valid_page_param
render_404 if current_page < 1
end
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index 01105f6cec4..d7c4d2fcda3 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -36,6 +36,10 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
@excluded = true
end
+ def control_behavior
+ # define a default nil control behavior so we can omit it when not needed
+ end
+
private
def feature_flag_name
diff --git a/app/experiments/concerns/project_commit_count.rb b/app/experiments/concerns/project_commit_count.rb
new file mode 100644
index 00000000000..706a1a24640
--- /dev/null
+++ b/app/experiments/concerns/project_commit_count.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ProjectCommitCount
+ include Gitlab::Git::WrapsGitalyErrors
+
+ def commit_count_for(project, default_count: 0, max_count: nil, **exception_details)
+ raw_repo = project.repository&.raw_repository
+ root_ref = raw_repo&.root_ref
+
+ return default_count unless root_ref
+
+ Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(root_ref, {
+ all: true, # include all branches
+ max_count: max_count # limit as an optimization
+ })
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, exception_details)
+
+ default_count
+ end
+end
diff --git a/app/experiments/empty_repo_upload_experiment.rb b/app/experiments/empty_repo_upload_experiment.rb
new file mode 100644
index 00000000000..d0d79a5fb45
--- /dev/null
+++ b/app/experiments/empty_repo_upload_experiment.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class EmptyRepoUploadExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ include ProjectCommitCount
+
+ TRACKING_START_DATE = DateTime.parse('2021/4/20')
+ INITIAL_COMMIT_COUNT = 1
+
+ def track_initial_write
+ return unless should_track? # early return if we don't need to ask for commit counts
+ return unless context.project.created_at > TRACKING_START_DATE # early return for older projects
+ return unless commit_count == INITIAL_COMMIT_COUNT
+
+ track(:initial_write, project: context.project)
+ end
+
+ private
+
+ def commit_count
+ commit_count_for(context.project, max_count: INITIAL_COMMIT_COUNT, experiment: name)
+ end
+end
diff --git a/app/experiments/in_product_guidance_environments_webide_experiment.rb b/app/experiments/in_product_guidance_environments_webide_experiment.rb
new file mode 100644
index 00000000000..d77063a9834
--- /dev/null
+++ b/app/experiments/in_product_guidance_environments_webide_experiment.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ exclude :has_environments?
+
+ def control_behavior
+ false
+ end
+
+ private
+
+ def has_environments?
+ !context.project.environments.empty?
+ end
+end
diff --git a/app/experiments/members/invite_email_experiment.rb b/app/experiments/members/invite_email_experiment.rb
index 6a7d2b110d3..f780c6962df 100644
--- a/app/experiments/members/invite_email_experiment.rb
+++ b/app/experiments/members/invite_email_experiment.rb
@@ -7,6 +7,10 @@ module Members
INVITE_TYPE = 'initial_email'
+ def self.initial_invite_email?(invite_type)
+ invite_type == INVITE_TYPE
+ end
+
def resolve_variant_name
RoundRobin.new(feature_flag_name, %i[avatar permission_info control]).execute
end
diff --git a/app/experiments/new_project_readme_experiment.rb b/app/experiments/new_project_readme_experiment.rb
index 8f88ad2adc1..c5c41330949 100644
--- a/app/experiments/new_project_readme_experiment.rb
+++ b/app/experiments/new_project_readme_experiment.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class NewProjectReadmeExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
- include Gitlab::Git::WrapsGitalyErrors
+ include ProjectCommitCount
INITIAL_WRITE_LIMIT = 3
EXPERIMENT_START_DATE = DateTime.parse('2021/1/20')
@@ -21,25 +21,18 @@ class NewProjectReadmeExperiment < ApplicationExperiment # rubocop:disable Gitla
def track_initial_writes(project)
return unless should_track? # early return if we don't need to ask for commit counts
return unless project.created_at > EXPERIMENT_START_DATE # early return for older projects
- return unless (commit_count = commit_count_for(project)) < INITIAL_WRITE_LIMIT
+ return unless (count = commit_count(project)) < INITIAL_WRITE_LIMIT
- track(:write, property: project.created_at.to_s, value: commit_count)
+ track(:write, property: project.created_at.to_s, value: count)
end
private
- def commit_count_for(project)
- raw_repo = project.repository&.raw_repository
- return INITIAL_WRITE_LIMIT unless raw_repo&.root_ref
-
- begin
- Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(raw_repo.root_ref, {
- all: true, # include all branches
- max_count: INITIAL_WRITE_LIMIT # limit as an optimization
- })
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, experiment: name)
- INITIAL_WRITE_LIMIT
- end
+ def commit_count(project)
+ commit_count_for(project,
+ default_count: INITIAL_WRITE_LIMIT,
+ max_count: INITIAL_WRITE_LIMIT,
+ experiment: name
+ )
end
end
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index 8e0444d324a..b4f66a38faa 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -67,4 +67,4 @@ module AlertManagement
end
end
-AlertManagement::AlertsFinder.prepend_if_ee('EE::AlertManagement::AlertsFinder')
+AlertManagement::AlertsFinder.prepend_mod_with('AlertManagement::AlertsFinder')
diff --git a/app/finders/alert_management/http_integrations_finder.rb b/app/finders/alert_management/http_integrations_finder.rb
index 5d4c9b6fbe3..e8e85da11b7 100644
--- a/app/finders/alert_management/http_integrations_finder.rb
+++ b/app/finders/alert_management/http_integrations_finder.rb
@@ -51,4 +51,4 @@ module AlertManagement
end
end
-::AlertManagement::HttpIntegrationsFinder.prepend_if_ee('EE::AlertManagement::HttpIntegrationsFinder')
+::AlertManagement::HttpIntegrationsFinder.prepend_mod_with('AlertManagement::HttpIntegrationsFinder')
diff --git a/app/finders/analytics/cycle_analytics/stage_finder.rb b/app/finders/analytics/cycle_analytics/stage_finder.rb
new file mode 100644
index 00000000000..732e9ff3e00
--- /dev/null
+++ b/app/finders/analytics/cycle_analytics/stage_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageFinder
+ def initialize(parent:, stage_id:)
+ @parent = parent
+ @stage_id = stage_id
+ end
+
+ def execute
+ build_in_memory_stage_by_name
+ end
+
+ private
+
+ attr_reader :parent, :stage_id
+
+ def build_in_memory_stage_by_name
+ parent.cycle_analytics_stages.build(find_in_memory_stage)
+ end
+
+ def find_in_memory_stage
+ # raise ActiveRecord::RecordNotFound, so it will behave similarly to AR models and produce 404 response in the controller
+ raw_stage = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.find do |hash|
+ hash[:name].eql?(stage_id)
+ end
+
+ raise(ActiveRecord::RecordNotFound, "Stage with id '#{stage_id}' could not be found") unless raw_stage
+
+ raw_stage
+ end
+ end
+ end
+end
+
+Analytics::CycleAnalytics::StageFinder.prepend_mod_with('Analytics::CycleAnalytics::StageFinder')
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
index ff5d9ea7d19..a9fffd3f411 100644
--- a/app/finders/autocomplete/users_finder.rb
+++ b/app/finders/autocomplete/users_finder.rb
@@ -102,4 +102,4 @@ module Autocomplete
end
end
-Autocomplete::UsersFinder.prepend_if_ee('EE::Autocomplete::UsersFinder')
+Autocomplete::UsersFinder.prepend_mod_with('Autocomplete::UsersFinder')
diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb
index 5ac1bbd0670..33aefe29392 100644
--- a/app/finders/ci/daily_build_group_report_results_finder.rb
+++ b/app/finders/ci/daily_build_group_report_results_finder.rb
@@ -94,4 +94,4 @@ module Ci
end
end
-Ci::DailyBuildGroupReportResultsFinder.prepend_if_ee('::EE::Ci::DailyBuildGroupReportResultsFinder')
+Ci::DailyBuildGroupReportResultsFinder.prepend_mod_with('Ci::DailyBuildGroupReportResultsFinder')
diff --git a/app/finders/ci/pipelines_finder.rb b/app/finders/ci/pipelines_finder.rb
index e509cf940b8..af7b23278a4 100644
--- a/app/finders/ci/pipelines_finder.rb
+++ b/app/finders/ci/pipelines_finder.rb
@@ -133,7 +133,7 @@ module Ci
when true
items.where.not(yaml_errors: nil)
when false
- items.where("yaml_errors IS NULL")
+ items.where(yaml_errors: nil)
else
items
end
diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb
index 1f6ee9d75ad..be65b1f6b3c 100644
--- a/app/finders/ci/pipelines_for_merge_request_finder.rb
+++ b/app/finders/ci/pipelines_for_merge_request_finder.rb
@@ -45,8 +45,12 @@ module Ci
private
+ # rubocop: disable CodeReuse/ActiveRecord
def pipelines_using_cte
- cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha))
+ sha_relation = merge_request.all_commits.select(:sha)
+ sha_relation = sha_relation.distinct if Feature.enabled?(:use_distinct_in_shas_cte)
+
+ cte = Gitlab::SQL::CTE.new(:shas, sha_relation)
pipelines_for_merge_requests = triggered_by_merge_request
pipelines_for_branch = filter_by_sha(triggered_for_branch, cte)
@@ -54,6 +58,7 @@ module Ci
Ci::Pipeline.with(cte.to_arel) # rubocop: disable CodeReuse/ActiveRecord
.from_union([pipelines_for_merge_requests, pipelines_for_branch])
end
+ # rubocop: enable CodeReuse/ActiveRecord
def filter_by_sha(pipelines, cte)
hex = Arel::Nodes::SqlLiteral.new("'hex'")
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index 1b76211c524..60dd977ff94 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -4,8 +4,6 @@ module Ci
class RunnersFinder < UnionFinder
include Gitlab::Allowable
- NUMBER_OF_RUNNERS_PER_PAGE = 30
-
def initialize(current_user:, group: nil, params:)
@params = params
@group = group
@@ -18,7 +16,6 @@ module Ci
filter_by_runner_type!
filter_by_tag_list!
sort!
- paginate!
@runners.with_tags
@@ -77,10 +74,6 @@ module Ci
@runners = @runners.order_by(sort_key)
end
- def paginate!
- @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
- end
-
def filter_by!(scope_name, available_scopes)
scope = @params[scope_name]
diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index 39c018818d1..f0ad998cadb 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -9,12 +9,16 @@ module Packages
private
+ def packages_for_project(project)
+ project.packages.installable
+ end
+
def packages_visible_to_user(user, within_group:)
return ::Packages::Package.none unless within_group
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
projects = projects_visible_to_reporters(user, within_group: within_group)
- ::Packages::Package.for_projects(projects.select(:id))
+ ::Packages::Package.for_projects(projects.select(:id)).installable
end
def projects_visible_to_user(user, within_group:)
@@ -25,7 +29,7 @@ module Packages
end
def projects_visible_to_reporters(user, within_group:)
- if user.is_a?(DeployToken) && Feature.enabled?(:packages_finder_helper_deploy_token)
+ if user.is_a?(DeployToken) && Feature.enabled?(:packages_finder_helper_deploy_token, default_enabled: :yaml)
user.accessible_projects
else
within_group.all_projects
@@ -38,7 +42,7 @@ module Packages
end
def filter_by_package_type(packages)
- return packages unless package_type
+ return packages.without_package_type(:terraform_module) unless package_type
raise InvalidPackageTypeError unless ::Packages::Package.package_types.key?(package_type)
packages.with_package_type(package_type)
@@ -50,6 +54,12 @@ module Packages
packages.search_by_name(params[:package_name])
end
+ def filter_by_package_version(packages)
+ return packages unless params[:package_version].present?
+
+ packages.with_version(params[:package_version])
+ end
+
def filter_with_version(packages)
return packages if params[:include_versionless].present?
diff --git a/app/finders/deploy_tokens/tokens_finder.rb b/app/finders/deploy_tokens/tokens_finder.rb
new file mode 100644
index 00000000000..98456628375
--- /dev/null
+++ b/app/finders/deploy_tokens/tokens_finder.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# Arguments:
+# current_user: The currently logged in user.
+# scope: A Project or Group to scope deploy tokens to (or :all for all tokens).
+# params:
+# active: Boolean - When true, only return active deployments.
+module DeployTokens
+ class TokensFinder
+ attr_reader :current_user, :params, :scope
+
+ def initialize(current_user, scope, params = {})
+ @current_user = current_user
+ @scope = scope
+ @params = params
+ end
+
+ def execute
+ by_active(init_collection)
+ end
+
+ private
+
+ def init_collection
+ case scope
+ when Group, Project
+ raise Gitlab::Access::AccessDeniedError unless current_user.can?(:read_deploy_token, scope)
+
+ scope.deploy_tokens
+ when :all
+ raise Gitlab::Access::AccessDeniedError unless current_user.can_read_all_resources?
+
+ DeployToken.all
+ else
+ raise ArgumentError, "Scope must be a Group, a Project, or the :all symbol."
+ end
+ end
+
+ def by_active(items)
+ params[:active] ? items.active : items
+ end
+ end
+end
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index ae26fc14ad5..acce038dba6 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -16,14 +16,25 @@
class DeploymentsFinder
attr_reader :params
+ # Warning:
+ # These const are directly used in Deployment Rest API, thus
+ # modifying these values could implicity change the API interface or introduce a breaking change.
+ # Also, if you add a sort value, make sure that the new query will stay
+ # performant with the other filtering/sorting parameters.
+ # The composed query could be significantly slower when the filtering and sorting columns are different.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/325627 for example.
ALLOWED_SORT_VALUES = %w[id iid created_at updated_at ref finished_at].freeze
DEFAULT_SORT_VALUE = 'id'
ALLOWED_SORT_DIRECTIONS = %w[asc desc].freeze
DEFAULT_SORT_DIRECTION = 'asc'
+ InefficientQueryError = Class.new(StandardError)
+
def initialize(params = {})
@params = params
+
+ validate!
end
def execute
@@ -38,15 +49,45 @@ class DeploymentsFinder
private
+ def validate!
+ if filter_by_updated_at? && filter_by_finished_at?
+ raise InefficientQueryError, 'Both `updated_at` filter and `finished_at` filter can not be specified'
+ end
+
+ # Currently, the inefficient parameters are allowed in order to avoid breaking changes in Deployment API.
+ # We'll switch to a hard error in https://gitlab.com/gitlab-org/gitlab/-/issues/328500.
+ if (filter_by_updated_at? && !order_by_updated_at?) || (!filter_by_updated_at? && order_by_updated_at?)
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
+ InefficientQueryError.new('`updated_at` filter and `updated_at` sorting must be paired')
+ )
+ end
+
+ if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?)
+ raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired'
+ end
+
+ if filter_by_finished_at? && !filter_by_successful_deployment?
+ raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.'
+ end
+
+ if params[:environment].present? && !params[:project].present?
+ raise InefficientQueryError, '`environment` filter must be combined with `project` scope.'
+ end
+ end
+
def init_collection
- if params[:project]
+ if params[:project].present?
params[:project].deployments
+ elsif params[:group].present?
+ ::Deployment.for_projects(params[:group].all_projects)
else
- Deployment.none
+ ::Deployment.none
end
end
def sort(items)
+ sort_params = build_sort_params
+ optimize_sort_params!(sort_params)
items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord
end
@@ -65,8 +106,8 @@ class DeploymentsFinder
end
def by_environment(items)
- if params[:environment].present?
- items.for_environment_name(params[:environment])
+ if params[:project].present? && params[:environment].present?
+ items.for_environment_name(params[:project], params[:environment])
else
items
end
@@ -82,14 +123,60 @@ class DeploymentsFinder
items.for_status(params[:status])
end
- def sort_params
+ def build_sort_params
order_by = ALLOWED_SORT_VALUES.include?(params[:order_by]) ? params[:order_by] : DEFAULT_SORT_VALUE
order_direction = ALLOWED_SORT_DIRECTIONS.include?(params[:sort]) ? params[:sort] : DEFAULT_SORT_DIRECTION
- { order_by => order_direction }.tap do |sort_values|
- sort_values['id'] = 'desc' if sort_values['updated_at']
- sort_values['id'] = sort_values.delete('created_at') if sort_values['created_at'] # Sorting by `id` produces the same result as sorting by `created_at`
+ { order_by => order_direction }
+ end
+
+ def optimize_sort_params!(sort_params)
+ sort_direction = sort_params.each_value.first
+
+ # Implicitly enforce the ordering when filtered by `updated_at` column for performance optimization.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/325627#note_552417509.
+ # We remove this in https://gitlab.com/gitlab-org/gitlab/-/issues/328500.
+ if filter_by_updated_at? && implicitly_enforce_ordering_for_updated_at_filter?
+ sort_params.replace('updated_at' => sort_direction)
end
+
+ if sort_params['created_at'] || sort_params['iid']
+ # Sorting by `id` produces the same result as sorting by `created_at` or `iid`
+ sort_params.replace(id: sort_direction)
+ elsif sort_params['updated_at']
+ # This adds the order as a tie-breaker when multiple rows have the same updated_at value.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20848.
+ sort_params.merge!(id: sort_direction)
+ end
+ end
+
+ def filter_by_updated_at?
+ params[:updated_before].present? || params[:updated_after].present?
+ end
+
+ def filter_by_finished_at?
+ params[:finished_before].present? || params[:finished_after].present?
+ end
+
+ def filter_by_successful_deployment?
+ params[:status].to_s == 'success'
+ end
+
+ def order_by_updated_at?
+ params[:order_by].to_s == 'updated_at'
+ end
+
+ def order_by_finished_at?
+ params[:order_by].to_s == 'finished_at'
+ end
+
+ def implicitly_enforce_ordering_for_updated_at_filter?
+ return false unless params[:project].present?
+
+ ::Feature.enabled?(
+ :deployments_finder_implicitly_enforce_ordering_for_updated_at_filter,
+ params[:project],
+ default_enabled: :yaml)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -113,5 +200,3 @@ class DeploymentsFinder
end
# rubocop: enable CodeReuse/ActiveRecord
end
-
-DeploymentsFinder.prepend_if_ee('EE::DeploymentsFinder')
diff --git a/app/finders/environment_names_finder.rb b/app/finders/environment_names_finder.rb
deleted file mode 100644
index e9063ef4c90..00000000000
--- a/app/finders/environment_names_finder.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-# Finder for obtaining the unique environment names of a project or group.
-#
-# This finder exists so that the merge requests "environments" filter can be
-# populated with a unique list of environment names. If we retrieve _just_ the
-# environments, duplicates may be present (e.g. multiple projects in a group
-# having a "staging" environment).
-#
-# In addition, this finder only produces unfoldered environments. We do this
-# because when searching for environments we want to exclude review app
-# environments.
-class EnvironmentNamesFinder
- attr_reader :project_or_group, :current_user
-
- def initialize(project_or_group, current_user = nil)
- @project_or_group = project_or_group
- @current_user = current_user
- end
-
- def execute
- all_environments.unfoldered.order_by_name.pluck_unique_names
- end
-
- def all_environments
- if project_or_group.is_a?(Namespace)
- namespace_environments
- else
- project_environments
- end
- end
-
- def namespace_environments
- # We assume reporter access is needed for the :read_environment permission
- # here. This expection is also present in
- # IssuableFinder::Params#min_access_level, which is used for filtering out
- # merge requests that don't have the right permissions.
- #
- # We use this approach so we don't need to load every project into memory
- # just to verify if we can see their environments. Doing so would not be
- # efficient, and possibly mess up pagination if certain projects are not
- # meant to be visible.
- projects = project_or_group
- .all_projects
- .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
-
- Environment.for_project(projects)
- end
-
- def project_environments
- if Ability.allowed?(current_user, :read_environment, project_or_group)
- project_or_group.environments
- else
- Environment.none
- end
- end
-end
diff --git a/app/finders/environments/environment_names_finder.rb b/app/finders/environments/environment_names_finder.rb
new file mode 100644
index 00000000000..d4928f0fc84
--- /dev/null
+++ b/app/finders/environments/environment_names_finder.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Environments
+ # Finder for obtaining the unique environment names of a project or group.
+ #
+ # This finder exists so that the merge requests "environments" filter can be
+ # populated with a unique list of environment names. If we retrieve _just_ the
+ # environments, duplicates may be present (e.g. multiple projects in a group
+ # having a "staging" environment).
+ #
+ # In addition, this finder only produces unfoldered environments. We do this
+ # because when searching for environments we want to exclude review app
+ # environments.
+ class EnvironmentNamesFinder
+ attr_reader :project_or_group, :current_user
+
+ def initialize(project_or_group, current_user = nil)
+ @project_or_group = project_or_group
+ @current_user = current_user
+ end
+
+ def execute
+ all_environments.unfoldered.order_by_name.pluck_unique_names
+ end
+
+ def all_environments
+ if project_or_group.is_a?(Namespace)
+ namespace_environments
+ else
+ project_environments
+ end
+ end
+
+ def namespace_environments
+ # We assume reporter access is needed for the :read_environment permission
+ # here. This expection is also present in
+ # IssuableFinder::Params#min_access_level, which is used for filtering out
+ # merge requests that don't have the right permissions.
+ #
+ # We use this approach so we don't need to load every project into memory
+ # just to verify if we can see their environments. Doing so would not be
+ # efficient, and possibly mess up pagination if certain projects are not
+ # meant to be visible.
+ projects = project_or_group
+ .all_projects
+ .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
+
+ Environment.for_project(projects)
+ end
+
+ def project_environments
+ if Ability.allowed?(current_user, :read_environment, project_or_group)
+ project_or_group.environments
+ else
+ Environment.none
+ end
+ end
+ end
+end
diff --git a/app/finders/environments/environments_by_deployments_finder.rb b/app/finders/environments/environments_by_deployments_finder.rb
new file mode 100644
index 00000000000..e0ecc98b1c0
--- /dev/null
+++ b/app/finders/environments/environments_by_deployments_finder.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Environments
+ class EnvironmentsByDeploymentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids)
+
+ if params[:find_latest]
+ find_one(environments.order_by_last_deployed_at_desc)
+ else
+ find_all(environments.order_by_last_deployed_at.to_a)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def find_one(environments)
+ [environments.find { |environment| valid_environment?(environment) }].compact
+ end
+
+ def find_all(environments)
+ environments.select { |environment| valid_environment?(environment) }
+ end
+
+ def valid_environment?(environment)
+ # Go in order of cost: SQL calls are cheaper than Gitaly calls
+ return false unless Ability.allowed?(current_user, :read_environment, environment)
+
+ return false if ref && params[:recently_updated] && !environment.recently_updated_on_branch?(ref)
+ return false if ref && commit && !environment.includes_commit?(commit)
+
+ true
+ end
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+ end
+end
diff --git a/app/finders/environments/environments_finder.rb b/app/finders/environments/environments_finder.rb
new file mode 100644
index 00000000000..190cdb3dec3
--- /dev/null
+++ b/app/finders/environments/environments_finder.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Environments
+ class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ InvalidStatesError = Class.new(StandardError)
+
+ def initialize(project, current_user, params = {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ environments = project.environments
+ environments = by_name(environments)
+ environments = by_search(environments)
+
+ # Raises InvalidStatesError if params[:states] contains invalid states.
+ by_states(environments)
+ end
+
+ private
+
+ def by_name(environments)
+ if params[:name].present?
+ environments.for_name(params[:name])
+ else
+ environments
+ end
+ end
+
+ def by_search(environments)
+ if params[:search].present?
+ environments.for_name_like(params[:search], limit: nil)
+ else
+ environments
+ end
+ end
+
+ def by_states(environments)
+ if params[:states].present?
+ environments_with_states(environments)
+ else
+ environments
+ end
+ end
+
+ def environments_with_states(environments)
+ # Convert to array of strings
+ states = Array(params[:states]).map(&:to_s)
+
+ raise InvalidStatesError, _('Requested states are invalid') unless valid_states?(states)
+
+ environments.with_states(states)
+ end
+
+ def valid_states?(states)
+ valid_states = Environment.valid_states.map(&:to_s)
+
+ (states - valid_states).empty?
+ end
+ end
+end
diff --git a/app/finders/environments_by_deployments_finder.rb b/app/finders/environments_by_deployments_finder.rb
deleted file mode 100644
index 76e1c050ea5..00000000000
--- a/app/finders/environments_by_deployments_finder.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-class EnvironmentsByDeploymentsFinder
- attr_reader :project, :current_user, :params
-
- def initialize(project, current_user, params = {})
- @project = project
- @current_user = current_user
- @params = params
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def execute
- deployments = project.deployments
- deployments =
- if ref
- deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
- deployments.where(deployments_query, ref: ref.to_s)
- elsif commit
- deployments.where(sha: commit.sha)
- else
- deployments.none
- end
-
- environment_ids = deployments
- .group(:environment_id)
- .select(:environment_id)
-
- environments = project.environments.available
- .where(id: environment_ids)
-
- if params[:find_latest]
- find_one(environments.order_by_last_deployed_at_desc)
- else
- find_all(environments.order_by_last_deployed_at.to_a)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def find_one(environments)
- [environments.find { |environment| valid_environment?(environment) }].compact
- end
-
- def find_all(environments)
- environments.select { |environment| valid_environment?(environment) }
- end
-
- def valid_environment?(environment)
- # Go in order of cost: SQL calls are cheaper than Gitaly calls
- return false unless Ability.allowed?(current_user, :read_environment, environment)
-
- return false if ref && params[:recently_updated] && !environment.recently_updated_on_branch?(ref)
- return false if ref && commit && !environment.includes_commit?(commit)
-
- true
- end
-
- def ref
- params[:ref].try(:to_s)
- end
-
- def commit
- params[:commit]
- end
-end
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
deleted file mode 100644
index c64e850f440..00000000000
--- a/app/finders/environments_finder.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# frozen_string_literal: true
-
-class EnvironmentsFinder
- attr_reader :project, :current_user, :params
-
- InvalidStatesError = Class.new(StandardError)
-
- def initialize(project, current_user, params = {})
- @project = project
- @current_user = current_user
- @params = params
- end
-
- def execute
- environments = project.environments
- environments = by_name(environments)
- environments = by_search(environments)
-
- # Raises InvalidStatesError if params[:states] contains invalid states.
- by_states(environments)
- end
-
- private
-
- def by_name(environments)
- if params[:name].present?
- environments.for_name(params[:name])
- else
- environments
- end
- end
-
- def by_search(environments)
- if params[:search].present?
- environments.for_name_like(params[:search], limit: nil)
- else
- environments
- end
- end
-
- def by_states(environments)
- if params[:states].present?
- environments_with_states(environments)
- else
- environments
- end
- end
-
- def environments_with_states(environments)
- # Convert to array of strings
- states = Array(params[:states]).map(&:to_s)
-
- raise InvalidStatesError, _('Requested states are invalid') unless valid_states?(states)
-
- environments.with_states(states)
- end
-
- def valid_states?(states)
- valid_states = Environment.valid_states.map(&:to_s)
-
- (states - valid_states).empty?
- end
-end
diff --git a/app/finders/fork_targets_finder.rb b/app/finders/fork_targets_finder.rb
index 719c244a207..3a79b216853 100644
--- a/app/finders/fork_targets_finder.rb
+++ b/app/finders/fork_targets_finder.rb
@@ -19,4 +19,4 @@ class ForkTargetsFinder
attr_reader :project, :user
end
-ForkTargetsFinder.prepend_if_ee('EE::ForkTargetsFinder')
+ForkTargetsFinder.prepend_mod_with('ForkTargetsFinder')
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index a6ecd835527..982234f7506 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -4,6 +4,12 @@ class GroupMembersFinder < UnionFinder
RELATIONS = %i(direct inherited descendants).freeze
DEFAULT_RELATIONS = %i(direct inherited).freeze
+ RELATIONS_DESCRIPTIONS = {
+ direct: 'Members in the group itself',
+ inherited: "Members in the group's ancestor groups",
+ descendants: "Members in the group's subgroups"
+ }.freeze
+
include CreatedAtFilter
# Params can be any of the following:
@@ -82,4 +88,4 @@ class GroupMembersFinder < UnionFinder
end
end
-GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
+GroupMembersFinder.prepend_mod_with('GroupMembersFinder')
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index dfdf821e3f0..d3c26fd845c 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -126,4 +126,4 @@ class GroupProjectsFinder < ProjectsFinder
end
end
-GroupProjectsFinder.prepend_if_ee('EE::GroupProjectsFinder')
+GroupProjectsFinder.prepend_mod_with('GroupProjectsFinder')
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 40a4e2b4f26..d1885b5ae08 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -142,8 +142,6 @@ class IssuableFinder
end
def should_filter_negated_args?
- return false unless not_filters_enabled?
-
# API endpoints send in `nil` values so we test if there are any non-nil
not_params.present? && not_params.values.any?
end
@@ -336,8 +334,7 @@ class IssuableFinder
return items if items.is_a?(ActiveRecord::NullRelation)
if use_cte_for_search?
- cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
- cte << items
+ cte = Gitlab::SQL::CTE.new(klass.table_name, items)
items = klass.with(cte.to_arel).from(klass.table_name)
end
@@ -370,8 +367,7 @@ class IssuableFinder
Issuables::AuthorFilter.new(
items,
params: original_params,
- or_filters_enabled: or_filters_enabled?,
- not_filters_enabled: not_filters_enabled?
+ or_filters_enabled: or_filters_enabled?
).filter
end
@@ -496,12 +492,6 @@ class IssuableFinder
end
end
- def not_filters_enabled?
- strong_memoize(:not_filters_enabled) do
- Feature.enabled?(:not_issuable_queries, feature_flag_scope, default_enabled: :yaml)
- end
- end
-
def feature_flag_scope
params.group || params.project
end
diff --git a/app/finders/issuables/author_filter.rb b/app/finders/issuables/author_filter.rb
index ce68dbafb95..522751a384e 100644
--- a/app/finders/issuables/author_filter.rb
+++ b/app/finders/issuables/author_filter.rb
@@ -27,7 +27,7 @@ module Issuables
end
def by_negated_author(issuables)
- return issuables unless not_filters_enabled? && not_params.present?
+ return issuables unless not_params.present?
if not_params[:author_id].present?
issuables.not_authored(not_params[:author_id])
diff --git a/app/finders/issuables/base_filter.rb b/app/finders/issuables/base_filter.rb
index 641ae2568cc..6d1a3f96062 100644
--- a/app/finders/issuables/base_filter.rb
+++ b/app/finders/issuables/base_filter.rb
@@ -4,11 +4,10 @@ module Issuables
class BaseFilter
attr_reader :issuables, :params
- def initialize(issuables, params:, or_filters_enabled: false, not_filters_enabled: false)
+ def initialize(issuables, params:, or_filters_enabled: false)
@issuables = issuables
@params = params
@or_filters_enabled = or_filters_enabled
- @not_filters_enabled = not_filters_enabled
end
def filter
@@ -28,9 +27,5 @@ module Issuables
def or_filters_enabled?
@or_filters_enabled
end
-
- def not_filters_enabled?
- @not_filters_enabled
- end
end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index e1a334413f8..eb9099fe256 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,7 +25,7 @@
# updated_after: datetime
# updated_before: datetime
# confidential: boolean
-# issue_type: array of strings (one of Issue.issue_types)
+# issue_types: array of strings (one of Issue.issue_types)
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
@@ -117,4 +117,4 @@ class IssuesFinder < IssuableFinder
end
end
-IssuesFinder.prepend_if_ee('EE::IssuesFinder')
+IssuesFinder.prepend_mod_with('IssuesFinder')
diff --git a/app/finders/issues_finder/params.rb b/app/finders/issues_finder/params.rb
index 668d969f7c0..1de117216f8 100644
--- a/app/finders/issues_finder/params.rb
+++ b/app/finders/issues_finder/params.rb
@@ -45,4 +45,4 @@ class IssuesFinder
end
end
-IssuesFinder::Params.prepend_if_ee('EE::IssuesFinder::Params')
+IssuesFinder::Params.prepend_mod_with('IssuesFinder::Params')
diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb
index c4cb33235af..b4235a77867 100644
--- a/app/finders/license_template_finder.rb
+++ b/app/finders/license_template_finder.rb
@@ -56,4 +56,4 @@ class LicenseTemplateFinder
end
end
-LicenseTemplateFinder.prepend_if_ee('::EE::LicenseTemplateFinder')
+LicenseTemplateFinder.prepend_mod_with('LicenseTemplateFinder')
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 9f9e2afa7fe..19fcd91a5b8 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -220,4 +220,4 @@ class MergeRequestsFinder < IssuableFinder
end
end
-MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder')
+MergeRequestsFinder.prepend_mod_with('MergeRequestsFinder')
diff --git a/app/finders/namespaces/projects_finder.rb b/app/finders/namespaces/projects_finder.rb
index bac5328d077..589a9696ea6 100644
--- a/app/finders/namespaces/projects_finder.rb
+++ b/app/finders/namespaces/projects_finder.rb
@@ -60,4 +60,4 @@ module Namespaces
end
end
-Namespaces::ProjectsFinder.prepend_if_ee('::EE::Namespaces::ProjectsFinder')
+Namespaces::ProjectsFinder.prepend_mod_with('Namespaces::ProjectsFinder')
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 96966001e85..42bd7a24888 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -183,4 +183,4 @@ class NotesFinder
end
end
-NotesFinder.prepend_if_ee('EE::NotesFinder')
+NotesFinder.prepend_mod_with('NotesFinder')
diff --git a/app/finders/packages/composer/packages_finder.rb b/app/finders/packages/composer/packages_finder.rb
index e63b2ee03fa..b5a1b19216f 100644
--- a/app/finders/packages/composer/packages_finder.rb
+++ b/app/finders/packages/composer/packages_finder.rb
@@ -9,7 +9,7 @@ module Packages
end
def execute
- packages_for_group_projects.composer.preload_composer
+ packages_for_group_projects(installable_only: true).composer.preload_composer
end
end
end
diff --git a/app/finders/packages/conan/package_finder.rb b/app/finders/packages/conan/package_finder.rb
index 26e9182f4e1..8ebdd358ba6 100644
--- a/app/finders/packages/conan/package_finder.rb
+++ b/app/finders/packages/conan/package_finder.rb
@@ -11,7 +11,7 @@ module Packages
end
def execute
- packages_for_current_user.with_name_like(query).order_name_asc if query
+ packages_for_current_user.installable.with_name_like(query).order_name_asc if query
end
private
diff --git a/app/finders/packages/generic/package_finder.rb b/app/finders/packages/generic/package_finder.rb
index 3a260e11fa3..8ec88754901 100644
--- a/app/finders/packages/generic/package_finder.rb
+++ b/app/finders/packages/generic/package_finder.rb
@@ -11,6 +11,7 @@ module Packages
project
.packages
.generic
+ .installable
.by_name_and_version!(package_name, package_version)
end
diff --git a/app/finders/packages/go/package_finder.rb b/app/finders/packages/go/package_finder.rb
index 4573417d11f..553e731895d 100644
--- a/app/finders/packages/go/package_finder.rb
+++ b/app/finders/packages/go/package_finder.rb
@@ -21,6 +21,7 @@ module Packages
@project
.packages
.golang
+ .installable
.with_name(@module_name)
.with_version(@module_version)
end
diff --git a/app/finders/packages/go/version_finder.rb b/app/finders/packages/go/version_finder.rb
index 6ee02b8c6f6..8500a441fb7 100644
--- a/app/finders/packages/go/version_finder.rb
+++ b/app/finders/packages/go/version_finder.rb
@@ -37,7 +37,7 @@ module Packages
@mod.version_by(commit: target)
else
- raise ArgumentError.new 'not a valid target'
+ raise ArgumentError, 'not a valid target'
end
end
end
diff --git a/app/finders/packages/group_or_project_package_finder.rb b/app/finders/packages/group_or_project_package_finder.rb
new file mode 100644
index 00000000000..fb8bcfc7d42
--- /dev/null
+++ b/app/finders/packages/group_or_project_package_finder.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Packages
+ class GroupOrProjectPackageFinder
+ include ::Packages::FinderHelper
+
+ def initialize(current_user, project_or_group, params = {})
+ @current_user = current_user
+ @project_or_group = project_or_group
+ @params = params
+ end
+
+ def execute
+ raise NotImplementedError
+ end
+
+ def execute!
+ raise NotImplementedError
+ end
+
+ private
+
+ def packages
+ raise NotImplementedError
+ end
+
+ def base
+ if project?
+ packages_for_project(@project_or_group)
+ elsif group?
+ packages_visible_to_user(@current_user, within_group: @project_or_group)
+ else
+ ::Packages::Package.none
+ end
+ end
+
+ def project?
+ @project_or_group.is_a?(::Project)
+ end
+
+ def group?
+ @project_or_group.is_a?(::Group)
+ end
+ end
+end
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index 8771bf90e75..e753fa4d455 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -20,19 +20,19 @@ module Packages
attr_reader :current_user, :group, :params
- def packages_for_group_projects
+ def packages_for_group_projects(installable_only: false)
packages = ::Packages::Package
.including_build_info
.including_project_route
.including_tags
.for_projects(group_projects_visible_to_current_user.select(:id))
- .processed
.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}")
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
- filter_by_status(packages)
+ packages = filter_by_package_version(packages)
+ installable_only ? packages.installable : filter_by_status(packages)
end
def group_projects_visible_to_current_user
diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb
index eefcdaba3c8..cc28d951f52 100644
--- a/app/finders/packages/maven/package_finder.rb
+++ b/app/finders/packages/maven/package_finder.rb
@@ -2,83 +2,23 @@
module Packages
module Maven
- class PackageFinder
- include ::Packages::FinderHelper
- include Gitlab::Utils::StrongMemoize
-
- def initialize(path, current_user, project: nil, group: nil, order_by_package_file: false)
- @path = path
- @current_user = current_user
- @project = project
- @group = group
- @order_by_package_file = order_by_package_file
- end
-
+ class PackageFinder < ::Packages::GroupOrProjectPackageFinder
def execute
- packages_with_path.last
+ packages.last
end
def execute!
- packages_with_path.last!
+ packages.last!
end
private
- def base
- if @project
- packages_for_a_single_project
- elsif @group
- packages_for_multiple_projects
- else
- ::Packages::Package.none
- end
- end
-
- def packages_with_path
- matching_packages = base.only_maven_packages_with_path(@path, use_cte: @group.present?)
-
- if group_level_improvements?
- matching_packages = matching_packages.order_by_package_file if @order_by_package_file
- else
- matching_packages = matching_packages.order_by_package_file if versionless_package?(matching_packages)
- end
+ def packages
+ matching_packages = base.only_maven_packages_with_path(@params[:path], use_cte: group?)
+ matching_packages = matching_packages.order_by_package_file if @params[:order_by_package_file]
matching_packages
end
-
- def versionless_package?(matching_packages)
- return if matching_packages.empty?
-
- # if one matching package is versionless, they all are.
- matching_packages.first&.version.nil?
- end
-
- # Produces a query that retrieves packages from a single project.
- def packages_for_a_single_project
- @project.packages
- end
-
- # Produces a query that retrieves packages from multiple projects that
- # the current user can view within a group.
- def packages_for_multiple_projects
- if group_level_improvements?
- packages_visible_to_user(@current_user, within_group: @group)
- else
- ::Packages::Package.for_projects(projects_visible_to_current_user)
- end
- end
-
- # Returns the projects that the current user can view within a group.
- def projects_visible_to_current_user
- @group.all_projects
- .public_or_visible_to_user(@current_user)
- end
-
- def group_level_improvements?
- strong_memoize(:group_level_improvements) do
- Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml)
- end
- end
end
end
end
diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb
index 3b79785d0e1..92ceac297ee 100644
--- a/app/finders/packages/npm/package_finder.rb
+++ b/app/finders/packages/npm/package_finder.rb
@@ -14,6 +14,7 @@ module Packages
def execute
base.npm
.with_name(@package_name)
+ .installable
.last_of_each_version
.preload_files
end
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index 2f66bd145ee..9ae52745bb2 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -2,51 +2,22 @@
module Packages
module Nuget
- class PackageFinder
- include ::Packages::FinderHelper
-
+ class PackageFinder < ::Packages::GroupOrProjectPackageFinder
MAX_PACKAGES_COUNT = 300
- def initialize(current_user, project_or_group, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT)
- @current_user = current_user
- @project_or_group = project_or_group
- @package_name = package_name
- @package_version = package_version
- @limit = limit
- end
-
def execute
- packages.limit_recent(@limit)
+ packages.limit_recent(@params[:limit] || MAX_PACKAGES_COUNT)
end
private
- def base
- if project?
- @project_or_group.packages
- elsif group?
- packages_visible_to_user(@current_user, within_group: @project_or_group)
- else
- ::Packages::Package.none
- end
- end
-
def packages
result = base.nuget
.has_version
- .processed
- .with_name_like(@package_name)
- result = result.with_version(@package_version) if @package_version.present?
+ .with_name_like(@params[:package_name])
+ result = result.with_version(@params[:package_version]) if @params[:package_version].present?
result
end
-
- def project?
- @project_or_group.is_a?(::Project)
- end
-
- def group?
- @project_or_group.is_a?(::Group)
- end
end
end
end
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index f1874b77845..ee96896e350 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -12,7 +12,7 @@ module Packages
.including_build_info
.including_project_route
.including_tags
- .processed
+ .displayable
.find(@package_id)
end
end
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index 840cbbf7b9d..552468ecfd1 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -17,7 +17,6 @@ module Packages
.including_build_info
.including_project_route
.including_tags
- .processed
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
diff --git a/app/finders/packages/pypi/package_finder.rb b/app/finders/packages/pypi/package_finder.rb
new file mode 100644
index 00000000000..574e9770363
--- /dev/null
+++ b/app/finders/packages/pypi/package_finder.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Packages
+ module Pypi
+ class PackageFinder < ::Packages::GroupOrProjectPackageFinder
+ def execute
+ packages.by_file_name_and_sha256(@params[:filename], @params[:sha256])
+ end
+
+ private
+
+ def packages
+ base.pypi.has_version
+ end
+ end
+ end
+end
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
new file mode 100644
index 00000000000..642ca2cf2e6
--- /dev/null
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Packages
+ module Pypi
+ class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
+ def execute!
+ results = packages.with_normalized_pypi_name(@params[:package_name])
+ raise ActiveRecord::RecordNotFound if results.empty?
+
+ results
+ end
+
+ private
+
+ def packages
+ base.pypi.has_version
+ end
+ end
+ end
+end
diff --git a/app/finders/projects/groups_finder.rb b/app/finders/projects/groups_finder.rb
index d0c42ad5611..fb98edabf58 100644
--- a/app/finders/projects/groups_finder.rb
+++ b/app/finders/projects/groups_finder.rb
@@ -7,6 +7,7 @@
# current_user - which user is requesting groups
# params:
# with_shared: boolean (optional)
+# shared_visible_only: boolean (optional)
# shared_min_access_level: integer (optional)
# skip_groups: array of integers (optional)
#
@@ -37,25 +38,35 @@ module Projects
Ability.allowed?(current_user, :read_project, project)
end
- # rubocop: disable CodeReuse/ActiveRecord
def all_groups
groups = []
- groups << project.group.self_and_ancestors if project.group
+ groups += [project.group.self_and_ancestors] if project.group
+ groups += with_shared_groups if params[:with_shared]
+
+ return [Group.none] if groups.compact.empty?
- if params[:with_shared]
- shared_groups = project.invited_groups
+ groups
+ end
- if params[:shared_min_access_level]
- shared_groups = shared_groups.where(
- 'project_group_links.group_access >= ?', params[:shared_min_access_level]
- )
- end
+ def with_shared_groups
+ shared_groups = project.invited_groups
+ shared_groups = apply_min_access_level(shared_groups)
- groups << shared_groups
+ if params[:shared_visible_only]
+ [
+ shared_groups.public_to_user(current_user),
+ shared_groups.for_authorized_group_members(current_user&.id)
+ ]
+ else
+ [shared_groups]
end
+ end
- groups << Group.none if groups.compact.empty?
- groups
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_min_access_level(groups)
+ return groups unless params[:shared_min_access_level]
+
+ groups.where('project_group_links.group_access >= ?', params[:shared_min_access_level])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/projects/members/effective_access_level_finder.rb b/app/finders/projects/members/effective_access_level_finder.rb
new file mode 100644
index 00000000000..2880d6667ce
--- /dev/null
+++ b/app/finders/projects/members/effective_access_level_finder.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Projects
+ module Members
+ class EffectiveAccessLevelFinder
+ include Gitlab::Utils::StrongMemoize
+
+ USER_ID_AND_ACCESS_LEVEL = [:user_id, :access_level].freeze
+ BATCH_SIZE = 5
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ return Member.none if no_members?
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ Member.from(generate_from_statement(user_ids_and_access_levels_from_all_memberships))
+ .select([:user_id, 'MAX(access_level) AS access_level'])
+ .group(:user_id)
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ attr_reader :project
+
+ def generate_from_statement(user_ids_and_access_levels)
+ "(VALUES #{generate_values_expression(user_ids_and_access_levels)}) members (user_id, access_level)"
+ end
+
+ def generate_values_expression(user_ids_and_access_levels)
+ user_ids_and_access_levels.map do |user_id, access_level|
+ "(#{user_id}, #{access_level})"
+ end.join(",")
+ end
+
+ def no_members?
+ user_ids_and_access_levels_from_all_memberships.blank?
+ end
+
+ def all_possible_avenues_of_membership
+ avenues = [authorizable_project_members]
+
+ avenues << if project.personal?
+ project_owner_acting_as_maintainer
+ else
+ authorizable_group_members
+ end
+
+ if include_membership_from_project_group_shares?
+ avenues << members_from_project_group_shares
+ end
+
+ avenues
+ end
+
+ # @return [Array<[user_id, access_level]>]
+ def user_ids_and_access_levels_from_all_memberships
+ strong_memoize(:user_ids_and_access_levels_from_all_memberships) do
+ all_possible_avenues_of_membership.flat_map do |relation|
+ relation.pluck(*USER_ID_AND_ACCESS_LEVEL) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+ end
+
+ def authorizable_project_members
+ project.members.authorizable
+ end
+
+ def authorizable_group_members
+ project.group.authorizable_members_with_parents
+ end
+
+ def members_from_project_group_shares
+ members = []
+
+ project.project_group_links.each_batch(of: BATCH_SIZE) do |relation|
+ members_per_batch = []
+
+ relation.includes(:group).each do |link| # rubocop: disable CodeReuse/ActiveRecord
+ members_per_batch << link.group.authorizable_members_with_parents.select(*user_id_and_access_level_for_project_group_shares(link))
+ end
+
+ members << Member.from_union(members_per_batch)
+ end
+
+ members.flatten
+ end
+
+ def project_owner_acting_as_maintainer
+ user_id = project.namespace.owner.id
+ access_level = Gitlab::Access::MAINTAINER
+
+ Member
+ .from(generate_from_statement([[user_id, access_level]])) # rubocop: disable CodeReuse/ActiveRecord
+ .limit(1)
+ end
+
+ def include_membership_from_project_group_shares?
+ project.allowed_to_share_with_group? && project.project_group_links.any?
+ end
+
+ # methods for `select` options
+
+ def user_id_and_access_level_for_project_group_shares(link)
+ least_access_level_among_group_membership_and_project_share =
+ smallest_value_arel([link.group_access, GroupMember.arel_table[:access_level]], 'access_level')
+
+ [
+ :user_id,
+ least_access_level_among_group_membership_and_project_share
+ ]
+ end
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel.sql(column_alias)
+ )
+ end
+ end
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 893e89daa3c..272747a124e 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -247,4 +247,4 @@ class ProjectsFinder < UnionFinder
end
end
-ProjectsFinder.prepend_if_ee('::EE::ProjectsFinder')
+ProjectsFinder.prepend_mod_with('ProjectsFinder')
diff --git a/app/finders/repositories/branch_names_finder.rb b/app/finders/repositories/branch_names_finder.rb
index 5bb67425aa5..8c8c7405407 100644
--- a/app/finders/repositories/branch_names_finder.rb
+++ b/app/finders/repositories/branch_names_finder.rb
@@ -10,9 +10,9 @@ module Repositories
end
def execute
- return unless search
+ return unless search && offset && limit
- repository.search_branch_names(search)
+ repository.search_branch_names(search).lazy.drop(offset).take(limit) # rubocop:disable CodeReuse/ActiveRecord
end
private
@@ -20,5 +20,13 @@ module Repositories
def search
@params[:search].presence
end
+
+ def offset
+ @params[:offset]
+ end
+
+ def limit
+ @params[:limit]
+ end
end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 941abb70400..81643826782 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -221,4 +221,4 @@ class SnippetsFinder < UnionFinder
end
end
-SnippetsFinder.prepend_if_ee('EE::SnippetsFinder')
+SnippetsFinder.prepend_mod_with('SnippetsFinder')
diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb
index 739beee236c..0f5622f2df0 100644
--- a/app/finders/template_finder.rb
+++ b/app/finders/template_finder.rb
@@ -7,7 +7,6 @@ class TemplateFinder
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate,
- gitlab_ci_syntax_ymls: ::Gitlab::Template::GitlabCiSyntaxYmlTemplate,
metrics_dashboard_ymls: ::Gitlab::Template::MetricsDashboardTemplate,
issues: ::Gitlab::Template::IssueTemplate,
merge_requests: ::Gitlab::Template::MergeRequestTemplate
@@ -71,4 +70,4 @@ class TemplateFinder
end
end
-TemplateFinder.prepend_if_ee('::EE::TemplateFinder')
+TemplateFinder.prepend_mod_with('TemplateFinder')
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index f28e1281488..e83018ed24c 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -215,4 +215,4 @@ class TodosFinder
end
end
-TodosFinder.prepend_if_ee('EE::TodosFinder')
+TodosFinder.prepend_mod_with('TodosFinder')
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index 5ac905e0dd4..8054ecbd502 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -143,4 +143,4 @@ class UsersFinder
# rubocop: enable CodeReuse/ActiveRecord
end
-UsersFinder.prepend_if_ee('EE::UsersFinder')
+UsersFinder.prepend_mod_with('UsersFinder')
diff --git a/app/finders/users_with_pending_todos_finder.rb b/app/finders/users_with_pending_todos_finder.rb
deleted file mode 100644
index 461bd92a366..00000000000
--- a/app/finders/users_with_pending_todos_finder.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# Finder that given a target (e.g. an issue) finds all the users that have
-# pending todos for said target.
-class UsersWithPendingTodosFinder
- attr_reader :target
-
- # target - The target, such as an Issue or MergeRequest.
- def initialize(target)
- @target = target
- end
-
- def execute
- User.for_todos(target.todos.pending)
- end
-end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 8369d0e120f..84941fcde02 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -10,6 +10,7 @@ class GitlabSchema < GraphQL::Schema
DEFAULT_MAX_DEPTH = 15
AUTHENTICATED_MAX_DEPTH = 20
+ use GraphQL::Subscriptions::ActionCableSubscriptions
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Pagination::Connections
@@ -24,6 +25,7 @@ class GitlabSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
+ subscription Types::SubscriptionType
default_max_page_size 100
@@ -168,7 +170,7 @@ class GitlabSchema < GraphQL::Schema
end
end
-GitlabSchema.prepend_if_ee('EE::GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
+GitlabSchema.prepend_mod_with('GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
# Force the schema to load as a workaround for intermittent errors we
# see due to a lack of thread safety.
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb
new file mode 100644
index 00000000000..671c7c2cd25
--- /dev/null
+++ b/app/graphql/graphql_triggers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module GraphqlTriggers
+ def self.issuable_assignees_updated(issuable)
+ GitlabSchema.subscriptions.trigger('issuableAssigneesUpdated', { issuable_id: issuable.to_gid }, issuable)
+ end
+end
diff --git a/app/graphql/mutations/alert_management/http_integration/create.rb b/app/graphql/mutations/alert_management/http_integration/create.rb
index 2d7bffb4333..54803855bcf 100644
--- a/app/graphql/mutations/alert_management/http_integration/create.rb
+++ b/app/graphql/mutations/alert_management/http_integration/create.rb
@@ -34,4 +34,4 @@ module Mutations
end
end
-Mutations::AlertManagement::HttpIntegration::Create.prepend_if_ee('::EE::Mutations::AlertManagement::HttpIntegration::Create')
+Mutations::AlertManagement::HttpIntegration::Create.prepend_mod_with('Mutations::AlertManagement::HttpIntegration::Create')
diff --git a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
index e33b7bb399a..efa92bfe895 100644
--- a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
+++ b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
@@ -33,4 +33,4 @@ module Mutations
end
end
-Mutations::AlertManagement::HttpIntegration::HttpIntegrationBase.prepend_if_ee('::EE::Mutations::AlertManagement::HttpIntegration::HttpIntegrationBase')
+Mutations::AlertManagement::HttpIntegration::HttpIntegrationBase.prepend_mod_with('Mutations::AlertManagement::HttpIntegration::HttpIntegrationBase')
diff --git a/app/graphql/mutations/alert_management/http_integration/update.rb b/app/graphql/mutations/alert_management/http_integration/update.rb
index b1e4ce841ee..4e6e7995c10 100644
--- a/app/graphql/mutations/alert_management/http_integration/update.rb
+++ b/app/graphql/mutations/alert_management/http_integration/update.rb
@@ -32,4 +32,4 @@ module Mutations
end
end
-Mutations::AlertManagement::HttpIntegration::Update.prepend_if_ee('::EE::Mutations::AlertManagement::HttpIntegration::Update')
+Mutations::AlertManagement::HttpIntegration::Update.prepend_mod_with('Mutations::AlertManagement::HttpIntegration::Update')
diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb
index 003c4f7761b..44fc22cf883 100644
--- a/app/graphql/mutations/boards/create.rb
+++ b/app/graphql/mutations/boards/create.rb
@@ -30,4 +30,4 @@ module Mutations
end
end
-Mutations::Boards::Create.prepend_if_ee('::EE::Mutations::Boards::Create')
+Mutations::Boards::Create.prepend_mod_with('Mutations::Boards::Create')
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index f32205643da..4c9752c6343 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -86,4 +86,4 @@ module Mutations
end
end
-Mutations::Boards::Issues::IssueMoveList.prepend_if_ee('EE::Mutations::Boards::Issues::IssueMoveList')
+Mutations::Boards::Issues::IssueMoveList.prepend_mod_with('Mutations::Boards::Issues::IssueMoveList')
diff --git a/app/graphql/mutations/boards/lists/base_update.rb b/app/graphql/mutations/boards/lists/base_update.rb
new file mode 100644
index 00000000000..b06cb3b1e32
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/base_update.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class BaseUpdate < BaseMutation
+ argument :position, GraphQL::INT_TYPE,
+ required: false,
+ description: 'Position of list within the board.'
+
+ argument :collapsed, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Indicates if the list is collapsed for this user.'
+
+ def resolve(list: nil, **args)
+ if list.nil? || !can_read_list?(list)
+ raise_resource_not_available_error!
+ end
+
+ update_result = update_list(list, args)
+
+ {
+ list: update_result.payload[:list],
+ errors: update_result.errors
+ }
+ end
+
+ private
+
+ def update_list(list, args)
+ raise NotImplementedError
+ end
+
+ def can_read_list?(list)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb
index 673fa95fc56..590a905ab7b 100644
--- a/app/graphql/mutations/boards/lists/create.rb
+++ b/app/graphql/mutations/boards/lists/create.rb
@@ -34,4 +34,4 @@ module Mutations
end
end
-Mutations::Boards::Lists::Create.prepend_if_ee('::EE::Mutations::Boards::Lists::Create')
+Mutations::Boards::Lists::Create.prepend_mod_with('Mutations::Boards::Lists::Create')
diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb
index 504082ec22c..d17dd5162a0 100644
--- a/app/graphql/mutations/boards/lists/update.rb
+++ b/app/graphql/mutations/boards/lists/update.rb
@@ -3,7 +3,7 @@
module Mutations
module Boards
module Lists
- class Update < BaseMutation
+ class Update < BaseUpdate
graphql_name 'UpdateBoardList'
argument :list_id, Types::GlobalIDType[List],
@@ -11,29 +11,11 @@ module Mutations
loads: Types::BoardListType,
description: 'Global ID of the list.'
- argument :position, GraphQL::INT_TYPE,
- required: false,
- description: 'Position of list within the board.'
-
- argument :collapsed, GraphQL::BOOLEAN_TYPE,
- required: false,
- description: 'Indicates if the list is collapsed for this user.'
-
field :list,
Types::BoardListType,
null: true,
description: 'Mutated list.'
- def resolve(list: nil, **args)
- raise_resource_not_available_error! unless can_read_list?(list)
- update_result = update_list(list, args)
-
- {
- list: update_result[:list],
- errors: list.errors.full_messages
- }
- end
-
private
def update_list(list, args)
@@ -42,8 +24,6 @@ module Mutations
end
def can_read_list?(list)
- return false unless list.present?
-
Ability.allowed?(current_user, :read_issue_board_list, list.board)
end
end
diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb
index 628b3a3fadb..f1a1d57306b 100644
--- a/app/graphql/mutations/boards/update.rb
+++ b/app/graphql/mutations/boards/update.rb
@@ -42,4 +42,4 @@ module Mutations
end
end
-Mutations::Boards::Update.prepend_if_ee('::EE::Mutations::Boards::Update')
+Mutations::Boards::Update.prepend_mod_with('Mutations::Boards::Update')
diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb
index d7451babaea..a484c2438a4 100644
--- a/app/graphql/mutations/ci/ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb
@@ -36,4 +36,4 @@ module Mutations
end
end
-Mutations::Ci::CiCdSettingsUpdate.prepend_if_ee('::EE::Mutations::Ci::CiCdSettingsUpdate')
+Mutations::Ci::CiCdSettingsUpdate.prepend_mod_with('Mutations::Ci::CiCdSettingsUpdate')
diff --git a/app/graphql/mutations/ci/job/base.rb b/app/graphql/mutations/ci/job/base.rb
new file mode 100644
index 00000000000..3359def159a
--- /dev/null
+++ b/app/graphql/mutations/ci/job/base.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Base < BaseMutation
+ JobID = ::Types::GlobalIDType[::Ci::Build]
+
+ argument :id, JobID,
+ required: true,
+ description: 'The ID of the job to mutate.'
+
+ def find_object(id: )
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = JobID.coerce_isolated_input(id)
+ GlobalID::Locator.locate(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/play.rb b/app/graphql/mutations/ci/job/play.rb
new file mode 100644
index 00000000000..f87904f8b25
--- /dev/null
+++ b/app/graphql/mutations/ci/job/play.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Play < Base
+ graphql_name 'JobPlay'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'The job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+ project = job.project
+
+ ::Ci::PlayBuildService.new(project, current_user).execute(job)
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/retry.rb b/app/graphql/mutations/ci/job/retry.rb
new file mode 100644
index 00000000000..a61d5dddb40
--- /dev/null
+++ b/app/graphql/mutations/ci/job/retry.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Retry < Base
+ graphql_name 'JobRetry'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'The job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+ project = job.project
+
+ ::Ci::RetryBuildService.new(project, current_user).execute(job)
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb
index 84933fee5d2..2e06e1ea0c4 100644
--- a/app/graphql/mutations/commits/create.rb
+++ b/app/graphql/mutations/commits/create.rb
@@ -5,6 +5,11 @@ module Mutations
class Create < BaseMutation
include FindsProject
+ class UrlHelpers
+ include GitlabRoutingHelper
+ include Gitlab::Routing
+ end
+
graphql_name 'CommitCreate'
argument :project_path, GraphQL::ID_TYPE,
@@ -29,6 +34,11 @@ module Mutations
required: true,
description: 'Array of action hashes to commit as a batch.'
+ field :commit_pipeline_path,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: "ETag path for the commit's pipeline."
+
field :commit,
Types::CommitType,
null: true,
@@ -50,6 +60,7 @@ module Mutations
{
commit: (project.repository.commit(result[:result]) if result[:status] == :success),
+ commit_pipeline_path: UrlHelpers.new.graphql_etag_pipeline_sha_path(result[:result]),
errors: Array.wrap(result[:message])
}
end
diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb
index d3ab0a1779a..e214a57500c 100644
--- a/app/graphql/mutations/concerns/mutations/assignable.rb
+++ b/app/graphql/mutations/concerns/mutations/assignable.rb
@@ -33,9 +33,9 @@ module Mutations
def assign!(resource, users, operation_mode)
update_service_class.new(
- resource.project,
- current_user,
- assignee_ids: assignee_ids(resource, users, operation_mode)
+ project: resource.project,
+ current_user: current_user,
+ params: { assignee_ids: assignee_ids(resource, users, operation_mode) }
).execute(resource)
end
diff --git a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
index 0fe2d09de6d..fd9031d3ea7 100644
--- a/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
+++ b/app/graphql/mutations/concerns/mutations/resolves_issuable.rb
@@ -38,4 +38,4 @@ module Mutations
end
end
-Mutations::ResolvesIssuable.prepend_if_ee('::EE::Mutations::ResolvesIssuable')
+Mutations::ResolvesIssuable.prepend_mod_with('Mutations::ResolvesIssuable')
diff --git a/app/graphql/mutations/issues/common_mutation_arguments.rb b/app/graphql/mutations/issues/common_mutation_arguments.rb
index 4b5b246281f..65768b85d14 100644
--- a/app/graphql/mutations/issues/common_mutation_arguments.rb
+++ b/app/graphql/mutations/issues/common_mutation_arguments.rb
@@ -22,6 +22,11 @@ module Mutations
as: :discussion_locked,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
+
+ argument :type, Types::IssueTypeEnum,
+ as: :issue_type,
+ required: false,
+ description: copy_field_description(Types::IssueType, :type)
end
end
end
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb
index 37fddd92832..3a57e2434a5 100644
--- a/app/graphql/mutations/issues/create.rb
+++ b/app/graphql/mutations/issues/create.rb
@@ -73,7 +73,7 @@ module Mutations
project = authorized_find!(project_path)
params = build_create_issue_params(attributes.merge(author_id: current_user.id))
- issue = ::Issues::CreateService.new(project, current_user, params).execute
+ issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute
if issue.spam?
issue.errors.add(:base, 'Spam detected.')
@@ -102,4 +102,4 @@ module Mutations
end
end
-Mutations::Issues::Create.prepend_if_ee('::EE::Mutations::Issues::Create')
+Mutations::Issues::Create.prepend_mod_with('Mutations::Issues::Create')
diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb
index 0f2af99bf61..cb4f0f42b38 100644
--- a/app/graphql/mutations/issues/move.rb
+++ b/app/graphql/mutations/issues/move.rb
@@ -18,7 +18,7 @@ module Mutations
target_project = resolve_project(full_path: target_project_path).sync
begin
- moved_issue = ::Issues::MoveService.new(source_project, current_user).execute(issue, target_project)
+ moved_issue = ::Issues::MoveService.new(project: source_project, current_user: current_user).execute(issue, target_project)
rescue ::Issues::MoveService::MoveError => error
errors = error.message
end
diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb
index 75befddc261..8e88b31d9ed 100644
--- a/app/graphql/mutations/issues/set_confidential.rb
+++ b/app/graphql/mutations/issues/set_confidential.rb
@@ -14,7 +14,7 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
- ::Issues::UpdateService.new(project, current_user, confidential: confidential)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential })
.execute(issue)
{
diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb
index da7892f4ed4..9cefac96b25 100644
--- a/app/graphql/mutations/issues/set_due_date.rb
+++ b/app/graphql/mutations/issues/set_due_date.rb
@@ -7,14 +7,23 @@ module Mutations
argument :due_date,
Types::TimeType,
- required: true,
- description: 'The desired due date for the issue.'
+ required: false,
+ description: 'The desired due date for the issue, ' \
+ 'due date will be removed if absent or set to null'
+
+ def ready?(**args)
+ unless args.key?(:due_date)
+ raise Gitlab::Graphql::Errors::ArgumentError, 'Argument dueDate must be provided (`null` accepted)'
+ end
+
+ super
+ end
def resolve(project_path:, iid:, due_date:)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
- ::Issues::UpdateService.new(project, current_user, due_date: due_date)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: { due_date: due_date })
.execute(issue)
{
diff --git a/app/graphql/mutations/issues/set_locked.rb b/app/graphql/mutations/issues/set_locked.rb
index 611226e48ad..3a696a64dad 100644
--- a/app/graphql/mutations/issues/set_locked.rb
+++ b/app/graphql/mutations/issues/set_locked.rb
@@ -13,7 +13,7 @@ module Mutations
def resolve(project_path:, iid:, locked:)
issue = authorized_find!(project_path: project_path, iid: iid)
- ::Issues::UpdateService.new(issue.project, current_user, discussion_locked: locked)
+ ::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: { discussion_locked: locked })
.execute(issue)
{
diff --git a/app/graphql/mutations/issues/set_severity.rb b/app/graphql/mutations/issues/set_severity.rb
index bc386e07178..778563ba053 100644
--- a/app/graphql/mutations/issues/set_severity.rb
+++ b/app/graphql/mutations/issues/set_severity.rb
@@ -12,7 +12,7 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
- ::Issues::UpdateService.new(project, current_user, severity: severity)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: { severity: severity })
.execute(issue)
{
diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb
index eea2cd49aa0..eb16b7b38d0 100644
--- a/app/graphql/mutations/issues/update.rb
+++ b/app/graphql/mutations/issues/update.rb
@@ -31,7 +31,7 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
- ::Issues::UpdateService.new(project, current_user, args).execute(issue)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: args).execute(issue)
{
issue: issue,
@@ -42,4 +42,4 @@ module Mutations
end
end
-Mutations::Issues::Update.prepend_if_ee('::EE::Mutations::Issues::Update')
+Mutations::Issues::Update.prepend_mod_with('Mutations::Issues::Update')
diff --git a/app/graphql/mutations/labels/create.rb b/app/graphql/mutations/labels/create.rb
index ccbd1c37cbf..4da628d53ea 100644
--- a/app/graphql/mutations/labels/create.rb
+++ b/app/graphql/mutations/labels/create.rb
@@ -20,10 +20,21 @@ module Mutations
required: false,
description: 'Description of the label.'
+ argument :remove_on_close, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: copy_field_description(Types::LabelType, :remove_on_close)
+
argument :color, GraphQL::STRING_TYPE,
required: false,
default_value: Label::DEFAULT_COLOR,
- description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords."
+ see: {
+ 'List of color keywords at mozilla.org' =>
+ 'https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords'
+ },
+ description: <<~DESC
+ The color of the label given in 6-digit hex notation with leading '#' sign
+ (for example, `#FFAABB`) or one of the CSS color names.
+ DESC
authorize :admin_label
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index da94dcd8890..9994f793a01 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -47,7 +47,7 @@ module Mutations
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.target_project
merge_params = args.compact.with_indifferent_access
- merge_service = ::MergeRequests::MergeService.new(project, current_user, merge_params)
+ merge_service = ::MergeRequests::MergeService.new(project: project, current_user: current_user, params: merge_params)
if error = validate(merge_request, merge_service, merge_params)
return { merge_request: merge_request, errors: [error] }
diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb
index 9ac8f70be95..4849c198677 100644
--- a/app/graphql/mutations/merge_requests/create.rb
+++ b/app/graphql/mutations/merge_requests/create.rb
@@ -42,7 +42,7 @@ module Mutations
project = authorized_find!(project_path)
params = attributes.merge(author_id: current_user.id)
- merge_request = ::MergeRequests::CreateService.new(project, current_user, params).execute
+ merge_request = ::MergeRequests::CreateService.new(project: project, current_user: current_user, params: params).execute
{
merge_request: merge_request.valid? ? merge_request : nil,
diff --git a/app/graphql/mutations/merge_requests/reviewer_rereview.rb b/app/graphql/mutations/merge_requests/reviewer_rereview.rb
index f6f4881654e..d1d5118e271 100644
--- a/app/graphql/mutations/merge_requests/reviewer_rereview.rb
+++ b/app/graphql/mutations/merge_requests/reviewer_rereview.rb
@@ -15,7 +15,7 @@ module Mutations
def resolve(project_path:, iid:, user:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
- result = ::MergeRequests::RequestReviewService.new(merge_request.project, current_user).execute(merge_request, user)
+ result = ::MergeRequests::RequestReviewService.new(project: merge_request.project, current_user: current_user).execute(merge_request, user)
{
merge_request: merge_request,
diff --git a/app/graphql/mutations/merge_requests/set_draft.rb b/app/graphql/mutations/merge_requests/set_draft.rb
new file mode 100644
index 00000000000..80006c6f70e
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_draft.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetDraft < Base
+ graphql_name 'MergeRequestSetDraft'
+
+ argument :draft,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: <<~DESC
+ Whether or not to set the merge request as a draft.
+ DESC
+
+ def resolve(project_path:, iid:, draft: nil)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(draft) })
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: errors_on_object(merge_request)
+ }
+ end
+
+ private
+
+ def wip_event(wip)
+ wip ? 'wip' : 'unwip'
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb
index 712c68c9425..a77c2731a05 100644
--- a/app/graphql/mutations/merge_requests/set_labels.rb
+++ b/app/graphql/mutations/merge_requests/set_labels.rb
@@ -9,14 +9,14 @@ module Mutations
[::Types::GlobalIDType[Label]],
required: true,
description: <<~DESC
- The Label IDs to set. Replaces existing labels by default.
+ The Label IDs to set. Replaces existing labels by default.
DESC
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
description: <<~DESC
- Changes the operation mode. Defaults to REPLACE.
+ Changes the operation mode. Defaults to REPLACE.
DESC
def resolve(project_path:, iid:, label_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
@@ -38,7 +38,7 @@ module Mutations
:label_ids
end
- ::MergeRequests::UpdateService.new(project, current_user, attribute_name => label_ids)
+ ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { attribute_name => label_ids })
.execute(merge_request)
{
diff --git a/app/graphql/mutations/merge_requests/set_locked.rb b/app/graphql/mutations/merge_requests/set_locked.rb
index c49d5186a03..e9e607551a6 100644
--- a/app/graphql/mutations/merge_requests/set_locked.rb
+++ b/app/graphql/mutations/merge_requests/set_locked.rb
@@ -9,14 +9,14 @@ module Mutations
GraphQL::BOOLEAN_TYPE,
required: true,
description: <<~DESC
- Whether or not to lock the merge request.
+ Whether or not to lock the merge request.
DESC
def resolve(project_path:, iid:, locked:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
- ::MergeRequests::UpdateService.new(project, current_user, discussion_locked: locked)
+ ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { discussion_locked: locked })
.execute(merge_request)
{
diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb
index abcb1bda1f3..ed5139c4af9 100644
--- a/app/graphql/mutations/merge_requests/set_milestone.rb
+++ b/app/graphql/mutations/merge_requests/set_milestone.rb
@@ -10,14 +10,14 @@ module Mutations
required: false,
loads: Types::MilestoneType,
description: <<~DESC
- The milestone to assign to the merge request.
+ The milestone to assign to the merge request.
DESC
def resolve(project_path:, iid:, milestone: nil)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
- ::MergeRequests::UpdateService.new(project, current_user, milestone: milestone)
+ ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { milestone: milestone })
.execute(merge_request)
{
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
index beb042ce93f..6f52b240840 100644
--- a/app/graphql/mutations/merge_requests/set_wip.rb
+++ b/app/graphql/mutations/merge_requests/set_wip.rb
@@ -16,7 +16,7 @@ module Mutations
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
- ::MergeRequests::UpdateService.new(project, current_user, wip_event: wip_event(merge_request, wip))
+ ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(merge_request, wip) })
.execute(merge_request)
{
diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb
index 6a94d2f37b2..246e468c34c 100644
--- a/app/graphql/mutations/merge_requests/update.rb
+++ b/app/graphql/mutations/merge_requests/update.rb
@@ -29,7 +29,7 @@ module Mutations
attributes = args.compact
::MergeRequests::UpdateService
- .new(merge_request.project, current_user, attributes)
+ .new(project: merge_request.project, current_user: current_user, params: attributes)
.execute(merge_request)
errors = errors_on_object(merge_request)
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index ca21c3418fc..75c80cfbd3e 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -25,6 +25,16 @@ module Mutations
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicate_exception_regex)
+ argument :generic_duplicates_allowed,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :generic_duplicates_allowed)
+
+ argument :generic_duplicate_exception_regex,
+ Types::UntrustedRegexp,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :generic_duplicate_exception_regex)
+
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,
diff --git a/app/graphql/mutations/security/ci_configuration/configure_sast.rb b/app/graphql/mutations/security/ci_configuration/configure_sast.rb
index e4a3f815396..237aff1f052 100644
--- a/app/graphql/mutations/security/ci_configuration/configure_sast.rb
+++ b/app/graphql/mutations/security/ci_configuration/configure_sast.rb
@@ -7,6 +7,11 @@ module Mutations
include FindsProject
graphql_name 'ConfigureSast'
+ description <<~DESC
+ Configure SAST for a project by enabling SAST in a new or modified
+ `.gitlab-ci.yml` file in a new branch. The new branch and a URL to
+ create a Merge Request are a part of the response.
+ DESC
argument :project_path, GraphQL::ID_TYPE,
required: true,
@@ -16,12 +21,12 @@ module Mutations
required: true,
description: 'SAST CI configuration for the project.'
- field :status, GraphQL::STRING_TYPE, null: false,
- description: 'Status of creating the commit for the supplied SAST CI configuration.'
-
field :success_path, GraphQL::STRING_TYPE, null: true,
description: 'Redirect path to use when the response is successful.'
+ field :branch, GraphQL::STRING_TYPE, null: true,
+ description: 'Branch that has the new/modified `.gitlab-ci.yml` file.'
+
authorize :push_code
def resolve(project_path:, configuration:)
@@ -35,9 +40,9 @@ module Mutations
def prepare_response(result)
{
- status: result[:status],
- success_path: result[:success_path],
- errors: Array(result[:errors])
+ branch: result.payload[:branch],
+ success_path: result.payload[:success_path],
+ errors: result.errors
}
end
end
diff --git a/app/graphql/mutations/security/ci_configuration/configure_secret_detection.rb b/app/graphql/mutations/security/ci_configuration/configure_secret_detection.rb
new file mode 100644
index 00000000000..32ad670edaa
--- /dev/null
+++ b/app/graphql/mutations/security/ci_configuration/configure_secret_detection.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Security
+ module CiConfiguration
+ class ConfigureSecretDetection < BaseMutation
+ include FindsProject
+
+ graphql_name 'ConfigureSecretDetection'
+ description <<~DESC
+ Configure Secret Detection for a project by enabling Secret Detection
+ in a new or modified `.gitlab-ci.yml` file in a new branch. The new
+ branch and a URL to create a Merge Request are a part of the
+ response.
+ DESC
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'Full path of the project.'
+
+ field :success_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Redirect path to use when the response is successful.'
+
+ field :branch, GraphQL::STRING_TYPE, null: true,
+ description: 'Branch that has the new/modified `.gitlab-ci.yml` file.'
+
+ authorize :push_code
+
+ def resolve(project_path:)
+ project = authorized_find!(project_path)
+
+ result = ::Security::CiConfiguration::SecretDetectionCreateService.new(project, current_user).execute
+ prepare_response(result)
+ end
+
+ private
+
+ def prepare_response(result)
+ {
+ branch: result.payload[:branch],
+ success_path: result.payload[:success_path],
+ errors: result.errors
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/queries/burndown_chart/burnup.query.graphql b/app/graphql/queries/burndown_chart/burnup.query.graphql
new file mode 100644
index 00000000000..7a389a6def5
--- /dev/null
+++ b/app/graphql/queries/burndown_chart/burnup.query.graphql
@@ -0,0 +1,70 @@
+query BurnupTimesSeriesData($id: ID!, $isIteration: Boolean = false, $weight: Boolean = false) {
+ milestone(id: $id) @skip(if: $isIteration) {
+ __typename
+ id
+ title
+ report {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+ iteration(id: $id) @include(if: $isIteration) {
+ __typename
+ id
+ title
+ report {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+}
diff --git a/app/graphql/queries/epic/epic_children.query.graphql b/app/graphql/queries/epic/epic_children.query.graphql
index c12778109d0..5ee27052f95 100644
--- a/app/graphql/queries/epic/epic_children.query.graphql
+++ b/app/graphql/queries/epic/epic_children.query.graphql
@@ -16,6 +16,10 @@ fragment RelatedTreeBaseEpic on Epic {
adminEpic
createEpic
}
+ descendantWeightSum {
+ closedIssues
+ openedIssues
+ }
descendantCounts {
__typename
openedEpics
diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
index 959bf7dc91d..873ecc81466 100644
--- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
+++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
@@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
__typename
id
iid
+ complete
usesNeeds
downstream {
__typename
diff --git a/app/graphql/resolvers/ci/runner_resolver.rb b/app/graphql/resolvers/ci/runner_resolver.rb
new file mode 100644
index 00000000000..ca94e28b2e9
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_resolver.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnerResolver < BaseResolver
+ include LooksAhead
+
+ type Types::Ci::RunnerType, null: true
+ extras [:lookahead]
+ description 'Runner information.'
+
+ argument :id,
+ type: ::Types::GlobalIDType[::Ci::Runner],
+ required: true,
+ description: 'Runner ID.'
+
+ def resolve_with_lookahead(id:)
+ find_runner(id: id)
+ end
+
+ private
+
+ def find_runner(id:)
+ runner_id = GitlabSchema.parse_gid(id, expected_type: ::Ci::Runner).model_id.to_i
+ preload_tag_list = lookahead.selects?(:tag_list)
+
+ BatchLoader::GraphQL.for(runner_id).batch(key: { preload_tag_list: preload_tag_list }) do |ids, loader, batch|
+ results = ::Ci::Runner.id_in(ids)
+ results = results.with_tags if batch[:key][:preload_tag_list]
+
+ results.each { |record| loader.call(record.id, record) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
new file mode 100644
index 00000000000..710706325cc
--- /dev/null
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnersResolver < BaseResolver
+ type Types::Ci::RunnerType.connection_type, null: true
+
+ argument :status, ::Types::Ci::RunnerStatusEnum,
+ required: false,
+ description: 'Filter runners by status.'
+
+ argument :type, ::Types::Ci::RunnerTypeEnum,
+ required: false,
+ description: 'Filter runners by type.'
+
+ argument :tag_list, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Filter by tags associated with the runner (comma-separated or array).'
+
+ argument :sort, ::Types::Ci::RunnerSortEnum,
+ required: false,
+ description: 'Sort order of results.'
+
+ def resolve(**args)
+ ::Ci::RunnersFinder
+ .new(current_user: current_user, params: runners_finder_params(args))
+ .execute
+ end
+
+ private
+
+ def runners_finder_params(params)
+ {
+ status_status: params[:status]&.to_s,
+ type_type: params[:type],
+ tag_name: params[:tag_list],
+ search: params[:search],
+ sort: params[:sort]&.to_s
+ }.compact
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/template_resolver.rb b/app/graphql/resolvers/ci/template_resolver.rb
new file mode 100644
index 00000000000..dd910116544
--- /dev/null
+++ b/app/graphql/resolvers/ci/template_resolver.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class TemplateResolver < BaseResolver
+ type Types::Ci::TemplateType, null: true
+
+ argument :name, GraphQL::STRING_TYPE, required: true,
+ description: 'Name of the CI/CD template to search for.'
+
+ alias_method :project, :object
+
+ def resolve(name: nil)
+ ::TemplateFinder.new(:gitlab_ci_ymls, project, name: name).execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb
index 3484a1cc4ba..88de69a3844 100644
--- a/app/graphql/resolvers/concerns/board_issue_filterable.rb
+++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb
@@ -32,4 +32,4 @@ module BoardIssueFilterable
end
end
-::BoardIssueFilterable.prepend_if_ee('::EE::Resolvers::BoardIssueFilterable')
+::BoardIssueFilterable.prepend_mod_with('Resolvers::BoardIssueFilterable')
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 0ff3997f3bc..aa08d62c6a5 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -7,57 +7,57 @@ module IssueResolverArguments
include LooksAhead
argument :iid, GraphQL::STRING_TYPE,
- required: false,
- description: 'IID of the issue. For example, "1".'
+ required: false,
+ description: 'IID of the issue. For example, "1".'
argument :iids, [GraphQL::STRING_TYPE],
- required: false,
- description: 'List of IIDs of issues. For example, [1, 2].'
+ required: false,
+ description: 'List of IIDs of issues. For example, ["1", "2"].'
argument :label_name, [GraphQL::STRING_TYPE, null: true],
- required: false,
- description: 'Labels applied to this issue.'
+ required: false,
+ description: 'Labels applied to this issue.'
argument :milestone_title, [GraphQL::STRING_TYPE, null: true],
- required: false,
- description: 'Milestone applied to this issue.'
+ required: false,
+ description: 'Milestone applied to this issue.'
argument :author_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of the author of the issue.'
+ required: false,
+ description: 'Username of the author of the issue.'
argument :assignee_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of a user assigned to the issue.',
- deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
+ required: false,
+ description: 'Username of a user assigned to the issue.',
+ deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
argument :assignee_usernames, [GraphQL::STRING_TYPE],
- required: false,
- description: 'Usernames of users assigned to the issue.'
+ required: false,
+ description: 'Usernames of users assigned to the issue.'
argument :assignee_id, GraphQL::STRING_TYPE,
- required: false,
- description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
+ required: false,
+ description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
argument :created_before, Types::TimeType,
- required: false,
- description: 'Issues created before this date.'
+ required: false,
+ description: 'Issues created before this date.'
argument :created_after, Types::TimeType,
- required: false,
- description: 'Issues created after this date.'
+ required: false,
+ description: 'Issues created after this date.'
argument :updated_before, Types::TimeType,
- required: false,
- description: 'Issues updated before this date.'
+ required: false,
+ description: 'Issues updated before this date.'
argument :updated_after, Types::TimeType,
- required: false,
- description: 'Issues updated after this date.'
+ required: false,
+ description: 'Issues updated after this date.'
argument :closed_before, Types::TimeType,
- required: false,
- description: 'Issues closed before this date.'
+ required: false,
+ description: 'Issues closed before this date.'
argument :closed_after, Types::TimeType,
- required: false,
- description: 'Issues closed after this date.'
+ required: false,
+ description: 'Issues closed after this date.'
argument :search, GraphQL::STRING_TYPE,
- required: false,
- description: 'Search query for issue title or description.'
+ required: false,
+ description: 'Search query for issue title or description.'
argument :types, [Types::IssueTypeEnum],
- as: :issue_types,
- description: 'Filter issues by the given issue types.',
- required: false
+ as: :issue_types,
+ description: 'Filter issues by the given issue types.',
+ required: false
argument :not, Types::Issues::NegatedIssueFilterInputType,
- description: 'List of negated params.',
+ description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
end
diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb
index 619448cbc18..08b29d884b0 100644
--- a/app/graphql/resolvers/design_management/versions_resolver.rb
+++ b/app/graphql/resolvers/design_management/versions_resolver.rb
@@ -62,6 +62,7 @@ module Resolvers
::DesignManagement::VersionsFinder
.new(design_or_collection, current_user, params)
.execute
+ .with_author
end
def by_id(gid)
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
index df04e70e250..ee604e7b307 100644
--- a/app/graphql/resolvers/environments_resolver.rb
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -21,8 +21,8 @@ module Resolvers
def resolve(**args)
return unless project.present?
- EnvironmentsFinder.new(project, context[:current_user], args).execute
- rescue EnvironmentsFinder::InvalidStatesError => exception
+ Environments::EnvironmentsFinder.new(project, context[:current_user], args).execute
+ rescue Environments::EnvironmentsFinder::InvalidStatesError => exception
raise Gitlab::Graphql::Errors::ArgumentError, exception.message
end
end
diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb
index d441cd80249..d91fe84317d 100644
--- a/app/graphql/resolvers/group_packages_resolver.rb
+++ b/app/graphql/resolvers/group_packages_resolver.rb
@@ -1,8 +1,19 @@
# frozen_string_literal: true
+# rubocop: disable Graphql/ResolverType
module Resolvers
- class GroupPackagesResolver < BaseResolver
- type Types::Packages::PackageType.connection_type, null: true
+ class GroupPackagesResolver < PackagesBaseResolver
+ # The GraphQL type is defined in the extended class
+
+ argument :sort, Types::Packages::PackageGroupSortEnum,
+ description: 'Sort packages by this criteria.',
+ required: false,
+ default_value: :created_desc
+
+ GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({
+ project_path_desc: { order_by: 'project_path', sort: 'desc' },
+ project_path_asc: { order_by: 'project_path', sort: 'asc' }
+ }).freeze
def ready?(**args)
context[self.class] ||= { executions: 0 }
@@ -12,16 +23,11 @@ module Resolvers
super
end
- def resolve(**args)
+ def resolve(sort:, **filters)
return unless packages_available?
- ::Packages::GroupPackagesFinder.new(current_user, object).execute
- end
-
- private
-
- def packages_available?
- ::Gitlab.config.packages.enabled
+ ::Packages::GroupPackagesFinder.new(current_user, object, filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))).execute
end
end
end
+# rubocop: enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 7a67f115abf..93e679b2d0c 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -55,4 +55,4 @@ module Resolvers
end
end
-Resolvers::IssuesResolver.prepend_if_ee('::EE::Resolvers::IssuesResolver')
+Resolvers::IssuesResolver.prepend_mod_with('Resolvers::IssuesResolver')
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
index 7320c3ce141..86286a744bd 100644
--- a/app/graphql/resolvers/namespace_projects_resolver.rb
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -64,4 +64,4 @@ module Resolvers
end
end
-Resolvers::NamespaceProjectsResolver.prepend_if_ee('::EE::Resolvers::NamespaceProjectsResolver')
+Resolvers::NamespaceProjectsResolver.prepend_mod_with('Resolvers::NamespaceProjectsResolver')
diff --git a/app/graphql/resolvers/packages_base_resolver.rb b/app/graphql/resolvers/packages_base_resolver.rb
new file mode 100644
index 00000000000..3378cc32c9f
--- /dev/null
+++ b/app/graphql/resolvers/packages_base_resolver.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class PackagesBaseResolver < BaseResolver
+ type Types::Packages::PackageType.connection_type, null: true
+
+ argument :sort, Types::Packages::PackageSortEnum,
+ description: 'Sort packages by this criteria.',
+ required: false,
+ default_value: :created_desc
+
+ argument :package_name, GraphQL::STRING_TYPE,
+ description: 'Search a package by name.',
+ required: false,
+ default_value: nil
+
+ argument :package_type, Types::Packages::PackageTypeEnum,
+ description: 'Filter a package by type.',
+ required: false,
+ default_value: nil
+
+ argument :status, Types::Packages::PackageStatusEnum,
+ description: 'Filter a package by status.',
+ required: false,
+ default_value: nil
+
+ argument :include_versionless, GraphQL::BOOLEAN_TYPE,
+ description: 'Include versionless packages.',
+ required: false,
+ default_value: false
+
+ SORT_TO_PARAMS_MAP = {
+ created_desc: { order_by: 'created', sort: 'desc' },
+ created_asc: { order_by: 'created', sort: 'asc' },
+ name_desc: { order_by: 'name', sort: 'desc' },
+ name_asc: { order_by: 'name', sort: 'asc' },
+ version_desc: { order_by: 'version', sort: 'desc' },
+ version_asc: { order_by: 'version', sort: 'asc' },
+ type_desc: { order_by: 'type', sort: 'desc' },
+ type_asc: { order_by: 'type', sort: 'asc' }
+ }.freeze
+
+ def resolve
+ raise NotImplementedError
+ end
+
+ private
+
+ def packages_available?
+ ::Gitlab.config.packages.enabled
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_packages_resolver.rb b/app/graphql/resolvers/project_packages_resolver.rb
index 288e14b41d0..6d66c2fe460 100644
--- a/app/graphql/resolvers/project_packages_resolver.rb
+++ b/app/graphql/resolvers/project_packages_resolver.rb
@@ -1,19 +1,15 @@
# frozen_string_literal: true
+# rubocop: disable Graphql/ResolverType
module Resolvers
- class ProjectPackagesResolver < BaseResolver
- type Types::Packages::PackageType.connection_type, null: true
+ class ProjectPackagesResolver < PackagesBaseResolver
+ # The GraphQL type is defined in the extended class
- def resolve(**args)
+ def resolve(sort:, **filters)
return unless packages_available?
- ::Packages::PackagesFinder.new(object).execute
- end
-
- private
-
- def packages_available?
- ::Gitlab.config.packages.enabled
+ ::Packages::PackagesFinder.new(object, filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))).execute
end
end
end
+# rubocop: enable Graphql/ResolverType
diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb
index ec31a7dbe6d..db3037ec591 100644
--- a/app/graphql/resolvers/projects/services_resolver.rb
+++ b/app/graphql/resolvers/projects/services_resolver.rb
@@ -21,7 +21,7 @@ module Resolvers
alias_method :project, :object
def resolve(active: nil, type: nil)
- servs = project.services
+ servs = project.integrations
servs = servs.by_active_flag(active) unless active.nil?
servs = servs.by_type(type) unless type.blank?
servs
diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb
index 20ef01f8593..67ff5fed0bb 100644
--- a/app/graphql/resolvers/release_resolver.rb
+++ b/app/graphql/resolvers/release_resolver.rb
@@ -15,8 +15,6 @@ module Resolvers
end
def resolve(tag_name:)
- return unless Feature.enabled?(:graphql_release_data, project, default_enabled: true)
-
ReleasesFinder.new(
project,
current_user,
diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb
index 01c1e9b11e7..358f3c33836 100644
--- a/app/graphql/resolvers/releases_resolver.rb
+++ b/app/graphql/resolvers/releases_resolver.rb
@@ -23,8 +23,6 @@ module Resolvers
}.freeze
def resolve(sort:)
- return unless Feature.enabled?(:graphql_release_data, project, default_enabled: true)
-
ReleasesFinder.new(
project,
current_user,
diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb
index 45cfe229b2f..c0a5ea0366f 100644
--- a/app/graphql/resolvers/repository_branch_names_resolver.rb
+++ b/app/graphql/resolvers/repository_branch_names_resolver.rb
@@ -10,8 +10,16 @@ module Resolvers
required: true,
description: 'The pattern to search for branch names by.'
- def resolve(search_pattern:)
- Repositories::BranchNamesFinder.new(object, search: search_pattern).execute
+ argument :offset, GraphQL::INT_TYPE,
+ required: true,
+ description: 'The number of branch names to skip.'
+
+ argument :limit, GraphQL::INT_TYPE,
+ required: true,
+ description: 'The number of branch names to return.'
+
+ def resolve(search_pattern:, offset:, limit:)
+ Repositories::BranchNamesFinder.new(object, offset: offset, limit: limit, search: search_pattern).execute
end
end
end
diff --git a/app/graphql/subscriptions/base_subscription.rb b/app/graphql/subscriptions/base_subscription.rb
new file mode 100644
index 00000000000..5f7931787df
--- /dev/null
+++ b/app/graphql/subscriptions/base_subscription.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Subscriptions
+ class BaseSubscription < GraphQL::Schema::Subscription
+ object_class Types::BaseObject
+ field_class Types::BaseField
+
+ def initialize(object:, context:, field:)
+ super
+
+ # Reset user so that we don't use a stale user for authorization
+ current_user.reset if current_user
+ end
+
+ def authorized?(*)
+ raise NotImplementedError
+ end
+
+ private
+
+ def unauthorized!
+ unsubscribe if context.query.subscription_update?
+
+ raise GraphQL::ExecutionError, 'Unauthorized subscription'
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+end
diff --git a/app/graphql/subscriptions/issuable_updated.rb b/app/graphql/subscriptions/issuable_updated.rb
new file mode 100644
index 00000000000..c1d82bfcf9c
--- /dev/null
+++ b/app/graphql/subscriptions/issuable_updated.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Subscriptions
+ class IssuableUpdated < BaseSubscription
+ include Gitlab::Graphql::Laziness
+
+ payload_type Types::IssuableType
+
+ argument :issuable_id, Types::GlobalIDType[Issuable],
+ required: true,
+ description: 'ID of the issuable.'
+
+ def subscribe(issuable_id:)
+ nil
+ end
+
+ def authorized?(issuable_id:)
+ # TODO: remove this check when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid IssuableID' unless issuable_id.is_a?(GlobalID)
+
+ issuable = force(GitlabSchema.find_by_gid(issuable_id))
+
+ unauthorized! unless issuable && Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
+
+ true
+ end
+ end
+end
diff --git a/app/graphql/types/access_level_enum.rb b/app/graphql/types/access_level_enum.rb
index b7eb35ddfc9..299952e4685 100644
--- a/app/graphql/types/access_level_enum.rb
+++ b/app/graphql/types/access_level_enum.rb
@@ -5,12 +5,12 @@ module Types
graphql_name 'AccessLevelEnum'
description 'Access level to a resource'
- value 'NO_ACCESS', value: Gitlab::Access::NO_ACCESS, description: 'No access'
- value 'MINIMAL_ACCESS', value: Gitlab::Access::MINIMAL_ACCESS, description: 'Minimal access'
- value 'GUEST', value: Gitlab::Access::GUEST, description: 'Guest access'
- value 'REPORTER', value: Gitlab::Access::REPORTER, description: 'Reporter access'
- value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: 'Developer access'
- value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: 'Maintainer access'
- value 'OWNER', value: Gitlab::Access::OWNER, description: 'Owner access'
+ value 'NO_ACCESS', value: Gitlab::Access::NO_ACCESS, description: 'No access.'
+ value 'MINIMAL_ACCESS', value: Gitlab::Access::MINIMAL_ACCESS, description: 'Minimal access.'
+ value 'GUEST', value: Gitlab::Access::GUEST, description: 'Guest access.'
+ value 'REPORTER', value: Gitlab::Access::REPORTER, description: 'Reporter access.'
+ value 'DEVELOPER', value: Gitlab::Access::DEVELOPER, description: 'Developer access.'
+ value 'MAINTAINER', value: Gitlab::Access::MAINTAINER, description: 'Maintainer access.'
+ value 'OWNER', value: Gitlab::Access::OWNER, description: 'Owner access.'
end
end
diff --git a/app/graphql/types/alert_management/http_integration_type.rb b/app/graphql/types/alert_management/http_integration_type.rb
index 0d5bb50a77c..bba9cb1bbfc 100644
--- a/app/graphql/types/alert_management/http_integration_type.rb
+++ b/app/graphql/types/alert_management/http_integration_type.rb
@@ -21,4 +21,4 @@ module Types
end
end
-Types::AlertManagement::HttpIntegrationType.prepend_ee_mod
+Types::AlertManagement::HttpIntegrationType.prepend_mod
diff --git a/app/graphql/types/alert_management/status_enum.rb b/app/graphql/types/alert_management/status_enum.rb
index 9d2c7316254..32a578cb155 100644
--- a/app/graphql/types/alert_management/status_enum.rb
+++ b/app/graphql/types/alert_management/status_enum.rb
@@ -7,7 +7,7 @@ module Types
description 'Alert status values'
::AlertManagement::Alert.status_names.each do |status|
- value status.to_s.upcase, value: status, description: "#{status.to_s.titleize} status"
+ value status.to_s.upcase, value: status, description: "#{::AlertManagement::Alert::STATUS_DESCRIPTIONS[status]}."
end
end
end
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index ff9a5a0611d..536a32f2bdd 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -4,10 +4,11 @@ module Types
class BaseArgument < GraphQL::Schema::Argument
include GitlabStyleDeprecations
- attr_reader :deprecation
+ attr_reader :deprecation, :doc_reference
def initialize(*args, **kwargs, &block)
@deprecation = gitlab_deprecation(kwargs)
+ @doc_reference = kwargs.delete(:see)
super(*args, **kwargs, &block)
end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index 518a902a5d7..7ef1cbddbd9 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -17,6 +17,9 @@ module Types
# declarative_enum MyDeclarativeEnum
# end
#
+ # Disabling descriptions rubocop for a false positive here
+ # rubocop: disable Graphql/Descriptions
+ #
def declarative_enum(enum_mod, use_name: true, use_description: true)
graphql_name(enum_mod.name) if use_name
description(enum_mod.description) if use_description
@@ -25,6 +28,7 @@ module Types
value(key.to_s.upcase, **content)
end
end
+ # rubocop: enable Graphql/Descriptions
# Helper to define an enum member for each element of a Rails AR enum
def from_rails_enum(enum, description:)
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 7c939f94dde..47caf83eb1c 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -8,10 +8,11 @@ module Types
DEFAULT_COMPLEXITY = 1
- attr_reader :deprecation
+ attr_reader :deprecation, :doc_reference
def initialize(**kwargs, &block)
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
+ @doc_reference = kwargs.delete(:see)
@constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0
@requires_argument = !!kwargs.delete(:requires_argument)
@authorize = Array.wrap(kwargs.delete(:authorize))
diff --git a/app/graphql/types/blob_viewer_type.rb b/app/graphql/types/blob_viewer_type.rb
new file mode 100644
index 00000000000..8d863c32bc7
--- /dev/null
+++ b/app/graphql/types/blob_viewer_type.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Types
+ class BlobViewerType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
+ graphql_name 'BlobViewer'
+ description 'Represents how the blob content should be displayed'
+
+ field :type, Types::BlobViewers::TypeEnum,
+ description: 'Type of blob viewer.',
+ null: false
+
+ field :load_async, GraphQL::BOOLEAN_TYPE,
+ description: 'Shows whether the blob content is loaded asynchronously.',
+ null: false
+
+ field :collapsed, GraphQL::BOOLEAN_TYPE,
+ description: 'Shows whether the blob should be displayed collapsed.',
+ method: :collapsed?,
+ null: false
+
+ field :too_large, GraphQL::BOOLEAN_TYPE,
+ description: 'Shows whether the blob is too large to be displayed.',
+ method: :too_large?,
+ null: false
+
+ field :render_error, GraphQL::STRING_TYPE,
+ description: 'Error rendering the blob content.',
+ null: true
+
+ field :file_type, GraphQL::STRING_TYPE,
+ description: 'Content file type.',
+ method: :partial_name,
+ null: false
+
+ field :loading_partial_name, GraphQL::STRING_TYPE,
+ description: 'Loading partial name.',
+ null: false
+
+ def collapsed
+ !!object&.collapsed?
+ end
+
+ def too_large
+ !!object&.too_large?
+ end
+ end
+end
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
index f215aa255de..dc10716dcb0 100644
--- a/app/graphql/types/board_list_type.rb
+++ b/app/graphql/types/board_list_type.rb
@@ -50,4 +50,4 @@ module Types
# rubocop: enable Graphql/AuthorizeTypes
end
-Types::BoardListType.prepend_if_ee('::EE::Types::BoardListType')
+Types::BoardListType.prepend_mod_with('Types::BoardListType')
diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb
index 42d8eecc366..292809b0d64 100644
--- a/app/graphql/types/board_type.rb
+++ b/app/graphql/types/board_type.rb
@@ -41,4 +41,4 @@ module Types
end
end
-Types::BoardType.prepend_if_ee('::EE::Types::BoardType')
+Types::BoardType.prepend_mod_with('Types::BoardType')
diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb
index 7cf2dcb9c82..633221e61d1 100644
--- a/app/graphql/types/boards/board_issue_input_base_type.rb
+++ b/app/graphql/types/boards/board_issue_input_base_type.rb
@@ -4,6 +4,10 @@ module Types
module Boards
# rubocop: disable Graphql/AuthorizeTypes
class BoardIssueInputBaseType < BoardIssuableInputBaseType
+ argument :iids, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'List of IIDs of issues. For example ["1", "2"].'
+
argument :milestone_title, GraphQL::STRING_TYPE,
required: false,
description: 'Filter by milestone title.'
@@ -19,4 +23,4 @@ module Types
end
end
-Types::Boards::BoardIssueInputBaseType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputBaseType')
+Types::Boards::BoardIssueInputBaseType.prepend_mod_with('Types::Boards::BoardIssueInputBaseType')
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
index 8c0e37e5cb7..7580b0378fe 100644
--- a/app/graphql/types/boards/board_issue_input_type.rb
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -8,10 +8,7 @@ module Types
argument :not, NegatedBoardIssueInputType,
required: false,
prepare: ->(negated_args, ctx) { negated_args.to_h },
- description: <<~MD
- List of negated arguments.
- Warning: this argument is experimental and a subject to change in future.
- MD
+ description: 'List of negated arguments.'
argument :search, GraphQL::STRING_TYPE,
required: false,
@@ -24,4 +21,4 @@ module Types
end
end
-Types::Boards::BoardIssueInputType.prepend_if_ee('::EE::Types::Boards::BoardIssueInputType')
+Types::Boards::BoardIssueInputType.prepend_mod_with('Types::Boards::BoardIssueInputType')
diff --git a/app/graphql/types/boards/negated_board_issue_input_type.rb b/app/graphql/types/boards/negated_board_issue_input_type.rb
index a0fab2ae969..834d94d4de6 100644
--- a/app/graphql/types/boards/negated_board_issue_input_type.rb
+++ b/app/graphql/types/boards/negated_board_issue_input_type.rb
@@ -7,4 +7,4 @@ module Types
end
end
-Types::Boards::NegatedBoardIssueInputType.prepend_if_ee('::EE::Types::Boards::NegatedBoardIssueInputType')
+Types::Boards::NegatedBoardIssueInputType.prepend_mod_with('Types::Boards::NegatedBoardIssueInputType')
diff --git a/app/graphql/types/ci/code_quality_degradation_severity_enum.rb b/app/graphql/types/ci/code_quality_degradation_severity_enum.rb
new file mode 100644
index 00000000000..742ac922198
--- /dev/null
+++ b/app/graphql/types/ci/code_quality_degradation_severity_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class CodeQualityDegradationSeverityEnum < BaseEnum
+ graphql_name 'CodeQualityDegradationSeverity'
+
+ ::Gitlab::Ci::Reports::CodequalityReports::SEVERITY_PRIORITIES.keys.each do |status|
+ value status.upcase,
+ description: "Code Quality degradation has a status of #{status}.",
+ value: status
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 94a256fed3d..5ed4d823aee 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -8,6 +8,8 @@ module Types
connection_type_class(Types::CountableConnectionType)
+ expose_permissions Types::PermissionTypes::Ci::Job
+
field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
description: 'ID of the job.'
field :pipeline, Types::Ci::PipelineType, null: true,
@@ -23,7 +25,7 @@ module Types
field :stage, Types::Ci::StageType, null: true,
description: 'Stage of the job.'
field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
- description: 'Whether this job is allowed to fail.'
+ description: 'Whether the job is allowed to fail.'
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the job in seconds.'
field :tags, [GraphQL::STRING_TYPE], null: true,
@@ -41,6 +43,12 @@ module Types
field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build.'
+ # Life-cycle durations:
+ field :queued_duration,
+ type: Types::DurationType,
+ null: true,
+ description: 'How long the job was enqueued before starting.'
+
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
@@ -63,8 +71,16 @@ module Types
description: 'Indicates the job can be canceled.'
field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?,
description: 'Indicates the job is active.'
+ field :stuck, GraphQL::BOOLEAN_TYPE, null: false, method: :stuck?,
+ description: 'Indicates the job is stuck.'
field :coverage, GraphQL::FLOAT_TYPE, null: true,
description: 'Coverage level of the job.'
+ field :created_by_tag, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Whether the job was created by a tag.'
+ field :manual_job, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Whether the job has a manual action.'
+ field :triggered, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Whether the job was triggered.'
def pipeline
Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find
@@ -123,6 +139,18 @@ module Types
def coverage
object&.coverage
end
+
+ def created_by_tag
+ object.tag?
+ end
+
+ def manual_job
+ object.try(:action?)
+ end
+
+ def triggered
+ object.try(:trigger_request)
+ end
end
end
end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index e0b2020dcc1..2800454a999 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -5,7 +5,7 @@ module Types
class PipelineStatusEnum < BaseEnum
::Ci::Pipeline.all_state_names.each do |state_symbol|
value state_symbol.to_s.upcase,
- description: ::Ci::Pipeline::STATUSES_DESCRIPTION[state_symbol],
+ description: "#{::Ci::Pipeline::STATUSES_DESCRIPTION[state_symbol]}.",
value: state_symbol.to_s
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 2e83f6c1f5a..2eeddaca6ba 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -24,6 +24,9 @@ module Types
field :before_sha, GraphQL::STRING_TYPE, null: true,
description: 'Base SHA of the source branch.'
+ field :complete, GraphQL::BOOLEAN_TYPE, null: false, method: :complete?,
+ description: 'Indicates if a pipeline is complete.'
+
field :status, PipelineStatusEnum, null: false,
description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})"
@@ -39,6 +42,9 @@ module Types
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the pipeline in seconds.'
+ field :queued_duration, Types::DurationType, null: true,
+ description: 'How long the pipeline was queued before starting.'
+
field :coverage, GraphQL::FLOAT_TYPE, null: true,
description: 'Coverage percentage.'
@@ -57,12 +63,17 @@ module Types
field :committed_at, Types::TimeType, null: true,
description: "Timestamp of the pipeline's commit."
- field :stages, Types::Ci::StageType.connection_type, null: true,
+ field :stages,
+ type: Types::Ci::StageType.connection_type,
+ null: true,
+ authorize: :read_commit_status,
description: 'Stages of the pipeline.',
extras: [:lookahead],
resolver: Resolvers::Ci::PipelineStagesResolver
- field :user, Types::UserType, null: true,
+ field :user,
+ type: Types::UserType,
+ null: true,
description: 'Pipeline user.'
field :retryable, GraphQL::BOOLEAN_TYPE,
@@ -78,12 +89,14 @@ module Types
field :jobs,
::Types::Ci::JobType.connection_type,
null: true,
+ authorize: :read_commit_status,
description: 'Jobs belonging to the pipeline.',
resolver: ::Resolvers::Ci::JobsResolver
field :job,
type: ::Types::Ci::JobType,
null: true,
+ authorize: :read_commit_status,
description: 'A specific job in this pipeline, either by name or ID.' do
argument :id,
type: ::Types::GlobalIDType[::CommitStatus],
@@ -95,7 +108,10 @@ module Types
description: 'Name of the job.'
end
- field :source_job, Types::Ci::JobType, null: true,
+ field :source_job,
+ type: Types::Ci::JobType,
+ null: true,
+ authorize: :read_commit_status,
description: 'Job where pipeline was triggered from.'
field :downstream, Types::Ci::PipelineType.connection_type, null: true,
@@ -166,4 +182,4 @@ module Types
end
end
-Types::Ci::PipelineType.prepend_if_ee('::EE::Types::Ci::PipelineType')
+Types::Ci::PipelineType.prepend_mod_with('Types::Ci::PipelineType')
diff --git a/app/graphql/types/ci/runner_access_level_enum.rb b/app/graphql/types/ci/runner_access_level_enum.rb
new file mode 100644
index 00000000000..e98f80336f1
--- /dev/null
+++ b/app/graphql/types/ci/runner_access_level_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerAccessLevelEnum < BaseEnum
+ graphql_name 'CiRunnerAccessLevel'
+
+ ::Ci::Runner.access_levels.keys.each do |type|
+ value type.upcase,
+ description: "A runner that is #{type.tr('_', ' ')}.",
+ value: type
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_sort_enum.rb b/app/graphql/types/ci/runner_sort_enum.rb
new file mode 100644
index 00000000000..550e870316a
--- /dev/null
+++ b/app/graphql/types/ci/runner_sort_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerSortEnum < BaseEnum
+ graphql_name 'CiRunnerSort'
+ description 'Values for sorting runners'
+
+ value 'CONTACTED_ASC', 'Ordered by contacted_at in ascending order.', value: :contacted_asc
+ value 'CREATED_DESC', 'Ordered by created_date in descending order.', value: :created_date
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
new file mode 100644
index 00000000000..ad69175e44a
--- /dev/null
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerStatusEnum < BaseEnum
+ graphql_name 'CiRunnerStatus'
+
+ ::Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ value status.to_s.upcase,
+ description: "A runner that is #{status.to_s.tr('_', ' ')}.",
+ value: status.to_sym
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
new file mode 100644
index 00000000000..3abed7289d5
--- /dev/null
+++ b/app/graphql/types/ci/runner_type.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerType < BaseObject
+ graphql_name 'CiRunner'
+ authorize :read_runner
+
+ field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
+ description: 'ID of the runner.'
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the runner.'
+ field :contacted_at, Types::TimeType, null: true,
+ description: 'Last contact from the runner.',
+ method: :contacted_at
+ field :maximum_timeout, GraphQL::INT_TYPE, null: true,
+ description: 'Maximum timeout (in seconds) for jobs processed by the runner.'
+ field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false,
+ description: 'Access level of the runner.'
+ field :active, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates the runner is allowed to receive jobs.'
+ field :status, ::Types::Ci::RunnerStatusEnum, null: false,
+ description: 'Status of the runner.'
+ field :version, GraphQL::STRING_TYPE, null: false,
+ description: 'Version of the runner.'
+ field :short_sha, GraphQL::STRING_TYPE, null: true,
+ description: %q(First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID.)
+ field :revision, GraphQL::STRING_TYPE, null: false,
+ description: 'Revision of the runner.'
+ field :locked, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates the runner is locked.'
+ field :run_untagged, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates the runner is able to run untagged jobs.'
+ field :ip_address, GraphQL::STRING_TYPE, null: false,
+ description: 'IP address of the runner.'
+ field :runner_type, ::Types::Ci::RunnerTypeEnum, null: false,
+ description: 'Type of the runner.'
+ field :tag_list, [GraphQL::STRING_TYPE], null: true,
+ description: 'Tags associated with the runner.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_type_enum.rb b/app/graphql/types/ci/runner_type_enum.rb
new file mode 100644
index 00000000000..f771635f4ed
--- /dev/null
+++ b/app/graphql/types/ci/runner_type_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerTypeEnum < BaseEnum
+ graphql_name 'CiRunnerType'
+
+ ::Ci::Runner.runner_types.keys.each do |type|
+ value type.upcase,
+ description: "A runner that is #{type.tr('_', ' ')}.",
+ value: type
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
index 56b4f248697..1be9e3192a9 100644
--- a/app/graphql/types/ci/stage_type.rb
+++ b/app/graphql/types/ci/stage_type.rb
@@ -2,20 +2,26 @@
module Types
module Ci
- # rubocop: disable Graphql/AuthorizeTypes
class StageType < BaseObject
graphql_name 'CiStage'
+ authorize :read_commit_status
- field :name, GraphQL::STRING_TYPE, null: true,
- description: 'Name of the stage.'
- field :groups, Ci::GroupType.connection_type, null: true,
- extras: [:lookahead],
- description: 'Group of jobs for the stage.'
- field :detailed_status, Types::Ci::DetailedStatusType, null: true,
- description: 'Detailed status of the stage.'
- field :jobs, Ci::JobType.connection_type, null: true,
- description: 'Jobs for the stage.',
- method: 'latest_statuses'
+ field :name,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Name of the stage.'
+ field :groups,
+ type: Ci::GroupType.connection_type,
+ null: true,
+ extras: [:lookahead],
+ description: 'Group of jobs for the stage.'
+ field :detailed_status, Types::Ci::DetailedStatusType,
+ null: true,
+ description: 'Detailed status of the stage.'
+ field :jobs, Ci::JobType.connection_type,
+ null: true,
+ description: 'Jobs for the stage.',
+ method: 'latest_statuses'
def detailed_status
object.detailed_status(current_user)
@@ -37,33 +43,6 @@ module Types
key = indexed[stage_id]
groups = ::Ci::Group.fabricate(project, key.stage, statuses)
- if Feature.enabled?(:ci_no_empty_groups, project)
- groups.each do |group|
- rejected = group.jobs.reject { |job| Ability.allowed?(current_user, :read_commit_status, job) }
- group.jobs.select! { |job| Ability.allowed?(current_user, :read_commit_status, job) }
- next unless group.jobs.empty?
-
- exc = StandardError.new('Empty Ci::Group')
- traces = rejected.map do |job|
- trace = []
- policy = Ability.policy_for(current_user, job)
- policy.debug(:read_commit_status, trace)
- trace
- end
- extra = {
- current_user_id: current_user&.id,
- project_id: project.id,
- pipeline_id: pl.id,
- stage_id: stage_id,
- group_name: group.name,
- rejected_job_ids: rejected.map(&:id),
- rejected_traces: traces
- }
- Gitlab::ErrorTracking.track_exception(exc, extra)
- end
- groups.reject! { |group| group.jobs.empty? }
- end
-
loader.call(key, groups)
end
end
diff --git a/app/graphql/types/ci/template_type.rb b/app/graphql/types/ci/template_type.rb
new file mode 100644
index 00000000000..5f07fa16928
--- /dev/null
+++ b/app/graphql/types/ci/template_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TemplateType < BaseObject
+ graphql_name 'CiTemplate'
+ description 'GitLab CI/CD configuration template.'
+
+ field :name, GraphQL::STRING_TYPE, null: false,
+ description: 'Name of the CI template.'
+ field :content, GraphQL::STRING_TYPE, null: false,
+ description: 'Contents of the CI template.'
+ end
+ end
+end
diff --git a/app/graphql/types/container_expiration_policy_cadence_enum.rb b/app/graphql/types/container_expiration_policy_cadence_enum.rb
index bb8bdf2197b..ac923f64b52 100644
--- a/app/graphql/types/container_expiration_policy_cadence_enum.rb
+++ b/app/graphql/types/container_expiration_policy_cadence_enum.rb
@@ -11,7 +11,7 @@ module Types
}.freeze
::ContainerExpirationPolicy.cadence_options.each do |option, description|
- value OPTIONS_MAPPING[option], description, value: option.to_s
+ value OPTIONS_MAPPING[option], description: description, value: option.to_s
end
end
end
diff --git a/app/graphql/types/container_expiration_policy_keep_enum.rb b/app/graphql/types/container_expiration_policy_keep_enum.rb
index 7632df61092..ca6fbbcf5ae 100644
--- a/app/graphql/types/container_expiration_policy_keep_enum.rb
+++ b/app/graphql/types/container_expiration_policy_keep_enum.rb
@@ -12,7 +12,7 @@ module Types
}.freeze
::ContainerExpirationPolicy.keep_n_options.each do |option, description|
- value OPTIONS_MAPPING[option], description, value: option
+ value OPTIONS_MAPPING[option], description: description, value: option
end
end
end
diff --git a/app/graphql/types/container_expiration_policy_older_than_enum.rb b/app/graphql/types/container_expiration_policy_older_than_enum.rb
index da70534b0d7..7364910f8cd 100644
--- a/app/graphql/types/container_expiration_policy_older_than_enum.rb
+++ b/app/graphql/types/container_expiration_policy_older_than_enum.rb
@@ -10,7 +10,7 @@ module Types
}.freeze
::ContainerExpirationPolicy.older_than_options.each do |option, description|
- value OPTIONS_MAPPING[option], description, value: option.to_s
+ value OPTIONS_MAPPING[option], description: description, value: option.to_s
end
end
end
diff --git a/app/graphql/types/design_management/version_type.rb b/app/graphql/types/design_management/version_type.rb
index 4bc71aef0f4..265d6185110 100644
--- a/app/graphql/types/design_management/version_type.rb
+++ b/app/graphql/types/design_management/version_type.rb
@@ -32,6 +32,10 @@ module Types
null: false,
description: 'A particular design as of this version, provided it is visible at this version.',
resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver.single
+
+ field :author, Types::UserType, null: false, description: 'Author of the version.'
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the version was created.'
end
end
end
diff --git a/app/graphql/types/duration_type.rb b/app/graphql/types/duration_type.rb
new file mode 100644
index 00000000000..260a2975ec1
--- /dev/null
+++ b/app/graphql/types/duration_type.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Types
+ class DurationType < BaseScalar
+ graphql_name 'Duration'
+ description <<~DESC
+ Duration between two instants, represented as a fractional number of seconds.
+
+ For example: 12.3334
+ DESC
+
+ def self.coerce_input(value, ctx)
+ case value
+ when Float
+ value
+ when Integer
+ value.to_f
+ when NilClass
+ raise GraphQL::CoercionError, 'Cannot be nil'
+ else
+ raise GraphQL::CoercionError, "Expected number: got #{value.class}"
+ end
+ end
+
+ def self.coerce_result(value, ctx)
+ value.to_f
+ end
+ end
+end
diff --git a/app/graphql/types/group_member_relation_enum.rb b/app/graphql/types/group_member_relation_enum.rb
index aa2e73d4944..54e2a175f33 100644
--- a/app/graphql/types/group_member_relation_enum.rb
+++ b/app/graphql/types/group_member_relation_enum.rb
@@ -6,7 +6,7 @@ module Types
description 'Group member relation'
::GroupMembersFinder::RELATIONS.each do |member_relation|
- value member_relation.to_s.upcase, value: member_relation, description: "#{member_relation.to_s.titleize} members"
+ value member_relation.to_s.upcase, value: member_relation, description: "#{::GroupMembersFinder::RELATIONS_DESCRIPTIONS[member_relation]}."
end
end
end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index a44281b2bdf..27f4ae47c41 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -144,7 +144,7 @@ module Types
resolver: Resolvers::GroupLabelsResolver
field :timelogs, ::Types::TimelogType.connection_type, null: false,
- description: 'Time logged on issues in the group and its subgroups.',
+ description: 'Time logged on issues and merge requests in the group and its subgroups.',
extras: [:lookahead],
complexity: 5,
resolver: ::Resolvers::TimelogResolver
@@ -169,4 +169,4 @@ module Types
end
end
-Types::GroupType.prepend_if_ee('EE::Types::GroupType')
+Types::GroupType.prepend_mod_with('Types::GroupType')
diff --git a/app/graphql/types/issuable_type.rb b/app/graphql/types/issuable_type.rb
new file mode 100644
index 00000000000..6ca74087f8a
--- /dev/null
+++ b/app/graphql/types/issuable_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ class IssuableType < BaseUnion
+ graphql_name 'Issuable'
+ description 'Represents an issuable.'
+
+ possible_types Types::IssueType, Types::MergeRequestType
+
+ def self.resolve_type(object, context)
+ case object
+ when Issue
+ Types::IssueType
+ when MergeRequest
+ Types::MergeRequestType
+ else
+ raise 'Unsupported issuable type'
+ end
+ end
+ end
+end
+
+Types::IssuableType.prepend_mod_with('Types::IssuableType')
diff --git a/app/graphql/types/issue_connection_type.rb b/app/graphql/types/issue_connection_type.rb
index 2e0f05f741e..7abb2253fac 100644
--- a/app/graphql/types/issue_connection_type.rb
+++ b/app/graphql/types/issue_connection_type.rb
@@ -6,4 +6,4 @@ module Types
end
end
-Types::IssueConnectionType.prepend_if_ee('::EE::Types::IssueConnectionType')
+Types::IssueConnectionType.prepend_mod_with('Types::IssueConnectionType')
diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb
index bf900fe3525..e730a51b60e 100644
--- a/app/graphql/types/issue_sort_enum.rb
+++ b/app/graphql/types/issue_sort_enum.rb
@@ -13,4 +13,4 @@ module Types
end
end
-Types::IssueSortEnum.prepend_if_ee('::EE::Types::IssueSortEnum')
+Types::IssueSortEnum.prepend_mod_with('Types::IssueSortEnum')
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 34c824fe9fb..0ccd1e2cebd 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -153,4 +153,4 @@ module Types
end
end
-Types::IssueType.prepend_if_ee('::EE::Types::IssueType')
+Types::IssueType.prepend_mod_with('Types::IssueType')
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
index 10bf6f21792..8a2e75ed9ba 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -24,4 +24,4 @@ module Types
end
end
-Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType')
+Types::Issues::NegatedIssueFilterInputType.prepend_mod_with('Types::Issues::NegatedIssueFilterInputType')
diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb
index 4e8718a80da..cb6b0312aa3 100644
--- a/app/graphql/types/label_type.rb
+++ b/app/graphql/types/label_type.rb
@@ -23,5 +23,7 @@ module Types
description: 'When this label was created.'
field :updated_at, Types::TimeType, null: false,
description: 'When this label was last updated.'
+ field :remove_on_close, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Whether the label should be removed from an issue when the issue is closed.'
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index c8ccf9d8aff..4eeeaa4f5d0 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -54,6 +54,9 @@ module Types
field :target_branch, GraphQL::STRING_TYPE, null: false,
description: 'Target branch of the merge request.'
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
+ deprecated: { reason: 'Use `draft`', milestone: '13.12' },
+ description: 'Indicates if the merge request is a draft.'
+ field :draft, GraphQL::BOOLEAN_TYPE, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.'
field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS).'
@@ -130,7 +133,10 @@ module Types
field :milestone, Types::MilestoneType, null: true,
description: 'The milestone of the merge request.'
- field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
+ field :assignees,
+ type: Types::MergeRequests::AssigneeType.connection_type,
+ null: true,
+ complexity: 5,
description: 'Assignees of the merge request.'
field :reviewers,
type: Types::MergeRequests::ReviewerType.connection_type,
@@ -257,4 +263,4 @@ module Types
end
end
-Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
+Types::MergeRequestType.prepend_mod_with('Types::MergeRequestType')
diff --git a/app/graphql/types/merge_requests/assignee_type.rb b/app/graphql/types/merge_requests/assignee_type.rb
new file mode 100644
index 00000000000..8448477370e
--- /dev/null
+++ b/app/graphql/types/merge_requests/assignee_type.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ class AssigneeType < ::Types::UserType
+ include FindClosest
+ include ::Types::MergeRequests::InteractsWithMergeRequest
+
+ graphql_name 'MergeRequestAssignee'
+ description 'A user assigned to a merge request.'
+ authorize :read_user
+ end
+ end
+end
diff --git a/app/graphql/types/merge_requests/interacts_with_merge_request.rb b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
new file mode 100644
index 00000000000..d685ac4d3c9
--- /dev/null
+++ b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ module InteractsWithMergeRequest
+ extend ActiveSupport::Concern
+
+ included do
+ field :merge_request_interaction,
+ type: ::Types::UserMergeRequestInteractionType,
+ null: true,
+ extras: [:parent],
+ description: "Details of this user's interactions with the merge request."
+ end
+
+ def merge_request_interaction(parent:)
+ merge_request = closest_parent(::Types::MergeRequestType, parent)
+ return unless merge_request
+
+ Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/merge_requests/reviewer_type.rb b/app/graphql/types/merge_requests/reviewer_type.rb
index 09ced39844a..1ced821c839 100644
--- a/app/graphql/types/merge_requests/reviewer_type.rb
+++ b/app/graphql/types/merge_requests/reviewer_type.rb
@@ -4,23 +4,11 @@ module Types
module MergeRequests
class ReviewerType < ::Types::UserType
include FindClosest
+ include ::Types::MergeRequests::InteractsWithMergeRequest
graphql_name 'MergeRequestReviewer'
- description 'A user from whom a merge request review has been requested.'
+ description 'A user assigned to a merge request as a reviewer.'
authorize :read_user
-
- field :merge_request_interaction,
- type: ::Types::UserMergeRequestInteractionType,
- null: true,
- extras: [:parent],
- description: "Details of this user's interactions with the merge request."
-
- def merge_request_interaction(parent:)
- merge_request = closest_parent(::Types::MergeRequestType, parent)
- return unless merge_request
-
- Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
- end
end
end
end
diff --git a/app/graphql/types/metadata/kas_type.rb b/app/graphql/types/metadata/kas_type.rb
new file mode 100644
index 00000000000..8af4c23270b
--- /dev/null
+++ b/app/graphql/types/metadata/kas_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module Metadata
+ class KasType < ::Types::BaseObject
+ graphql_name 'Kas'
+
+ authorize :read_instance_metadata
+
+ field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates whether the Kubernetes Agent Server is enabled.'
+ field :version, GraphQL::STRING_TYPE, null: true,
+ description: 'KAS version.'
+ field :external_url, GraphQL::STRING_TYPE, null: true,
+ description: 'The URL used by the Agents to communicate with KAS.'
+ end
+ end
+end
diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb
index 0c360d4f292..851c2a3f1e3 100644
--- a/app/graphql/types/metadata_type.rb
+++ b/app/graphql/types/metadata_type.rb
@@ -10,5 +10,7 @@ module Types
description: 'Version.'
field :revision, GraphQL::STRING_TYPE, null: false,
description: 'Revision.'
+ field :kas, ::Types::Metadata::KasType, null: false,
+ description: 'Metadata about KAS.'
end
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index 91a5109c748..eafede26c9e 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -57,11 +57,9 @@ module Types
description: 'Milestone statistics.'
def stats
- return unless Feature.enabled?(:graphql_milestone_stats, milestone.project || milestone.group, default_enabled: true)
-
milestone
end
end
end
-Types::MilestoneType.prepend_if_ee('::EE::Types::MilestoneType')
+Types::MilestoneType.prepend_mod_with('Types::MilestoneType')
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 5a9c7b32deb..54a06ed5342 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -16,6 +16,7 @@ module Types
mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken
mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy
mount_mutation Mutations::Security::CiConfiguration::ConfigureSast
+ mount_mutation Mutations::Security::CiConfiguration::ConfigureSecretDetection
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update
mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken
@@ -51,7 +52,10 @@ module Types
mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription
- mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
+ mount_mutation Mutations::MergeRequests::SetWip,
+ calls_gitaly: true,
+ deprecated: { reason: 'Use mergeRequestSetDraft', milestone: '13.12' }
+ mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview
mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
@@ -93,10 +97,12 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry
mount_mutation Mutations::Ci::CiCdSettingsUpdate
+ mount_mutation Mutations::Ci::Job::Play
+ mount_mutation Mutations::Ci::Job::Retry
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create
end
end
::Types::MutationType.prepend(::Types::DeprecatedMutations)
-::Types::MutationType.prepend_if_ee('::EE::Types::MutationType')
+::Types::MutationType.prepend_mod_with('Types::MutationType')
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index 0720a1cfb4b..af091515979 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -10,5 +10,7 @@ module Types
field :maven_duplicates_allowed, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
field :maven_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
+ field :generic_duplicates_allowed, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
+ field :generic_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
end
end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index da983399a11..96eff8a46b0 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -48,4 +48,4 @@ module Types
end
end
-Types::NamespaceType.prepend_if_ee('EE::Types::NamespaceType')
+Types::NamespaceType.prepend_mod_with('Types::NamespaceType')
diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb
index f8626d249a1..a82a76f9c87 100644
--- a/app/graphql/types/notes/noteable_type.rb
+++ b/app/graphql/types/notes/noteable_type.rb
@@ -28,4 +28,4 @@ module Types
end
end
-Types::Notes::NoteableType.prepend_if_ee('::EE::Types::Notes::NoteableType')
+Types::Notes::NoteableType.prepend_mod_with('Types::Notes::NoteableType')
diff --git a/app/graphql/types/notes/position_type_enum.rb b/app/graphql/types/notes/position_type_enum.rb
index 9939f6511ce..18934636670 100644
--- a/app/graphql/types/notes/position_type_enum.rb
+++ b/app/graphql/types/notes/position_type_enum.rb
@@ -6,8 +6,8 @@ module Types
graphql_name 'DiffPositionType'
description 'Type of file the position refers to'
- value 'text', description: "A text file"
- value 'image', description: "An image"
+ value 'text', description: "A text file."
+ value 'image', description: "An image."
end
end
end
diff --git a/app/graphql/types/packages/maven/metadatum_type.rb b/app/graphql/types/packages/maven/metadatum_type.rb
new file mode 100644
index 00000000000..bdb250ef96b
--- /dev/null
+++ b/app/graphql/types/packages/maven/metadatum_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Maven
+ class MetadatumType < BaseObject
+ graphql_name 'MavenMetadata'
+ description 'Maven metadata'
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Maven::Metadatum], null: false, description: 'ID of the metadatum.'
+ field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
+ field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
+ field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the Maven package.'
+ field :app_group, GraphQL::STRING_TYPE, null: false, description: 'App group of the Maven package.'
+ field :app_version, GraphQL::STRING_TYPE, null: true, description: 'App version of the Maven package.'
+ field :app_name, GraphQL::STRING_TYPE, null: false, description: 'App name of the Maven package.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/metadata_type.rb b/app/graphql/types/packages/metadata_type.rb
index 4ab6707df88..94880cb9b22 100644
--- a/app/graphql/types/packages/metadata_type.rb
+++ b/app/graphql/types/packages/metadata_type.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'PackageMetadata'
description 'Represents metadata associated with a Package'
- possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType
+ possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType, ::Types::Packages::Maven::MetadatumType, ::Types::Packages::Nuget::MetadatumType
def self.resolve_type(object, context)
case object
@@ -14,6 +14,10 @@ module Types
::Types::Packages::Composer::MetadatumType
when ::Packages::Conan::Metadatum
::Types::Packages::Conan::MetadatumType
+ when ::Packages::Maven::Metadatum
+ ::Types::Packages::Maven::MetadatumType
+ when ::Packages::Nuget::Metadatum
+ ::Types::Packages::Nuget::MetadatumType
else
# NOTE: This method must be kept in sync with `PackageWithoutVersionsType#metadata`,
# which must never produce data that this discriminator cannot handle.
diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb
new file mode 100644
index 00000000000..63fae2fb197
--- /dev/null
+++ b/app/graphql/types/packages/nuget/metadatum_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Nuget
+ class MetadatumType < BaseObject
+ graphql_name 'NugetMetadata'
+ description 'Nuget metadata'
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Nuget::Metadatum], null: false, description: 'ID of the metadatum.'
+ field :license_url, GraphQL::STRING_TYPE, null: false, description: 'License URL of the Nuget package.'
+ field :project_url, GraphQL::STRING_TYPE, null: false, description: 'Project URL of the Nuget package.'
+ field :icon_url, GraphQL::STRING_TYPE, null: false, description: 'Icon URL of the Nuget package.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_group_sort_enum.rb b/app/graphql/types/packages/package_group_sort_enum.rb
new file mode 100644
index 00000000000..70fb27ec0db
--- /dev/null
+++ b/app/graphql/types/packages/package_group_sort_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageGroupSortEnum < PackageSortEnum
+ graphql_name 'PackageGroupSort'
+ description 'Values for sorting group packages'
+
+ # The following enums are not available till we enable the new Arel node:
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58657#note_552632305
+ # value 'PROJECT_PATH_DESC', 'Project by descending order.', value: :project_path_desc
+ # value 'PROJECT_PATH_ASC', 'Project by ascending order.', value: :project_path_asc
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_sort_enum.rb b/app/graphql/types/packages/package_sort_enum.rb
new file mode 100644
index 00000000000..ee14cf7a9e6
--- /dev/null
+++ b/app/graphql/types/packages/package_sort_enum.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageSortEnum < BaseEnum
+ graphql_name 'PackageSort'
+ description 'Values for sorting package'
+
+ value 'CREATED_DESC', 'Ordered by created_at in descending order.', value: :created_desc
+ value 'CREATED_ASC', 'Ordered by created_at in ascending order.', value: :created_asc
+ value 'NAME_DESC', 'Ordered by name in descending order.', value: :name_desc
+ value 'NAME_ASC', 'Ordered by name in ascending order.', value: :name_asc
+ value 'VERSION_DESC', 'Ordered by version in descending order.', value: :version_desc
+ value 'VERSION_ASC', 'Ordered by version in ascending order.', value: :version_asc
+ value 'TYPE_DESC', 'Ordered by type in descending order.', value: :type_desc
+ value 'TYPE_ASC', 'Ordered by type in ascending order.', value: :type_asc
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_status_enum.rb b/app/graphql/types/packages/package_status_enum.rb
new file mode 100644
index 00000000000..2e6ea5d0a50
--- /dev/null
+++ b/app/graphql/types/packages/package_status_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageStatusEnum < BaseEnum
+ graphql_name 'PackageStatus'
+
+ ::Packages::Package.statuses.keys.each do |status|
+ value status.to_s.upcase, description: "Packages with a #{status} status", value: status.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index a263ca1577a..b349b655fa5 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -25,6 +25,7 @@ module Types
field :versions, ::Types::Packages::PackageType.connection_type, null: true,
description: 'The other versions of the package.',
deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' }
+ field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
@@ -44,6 +45,10 @@ module Types
object.composer_metadatum
when 'conan'
object.conan_metadatum
+ when 'maven'
+ object.maven_metadatum
+ when 'nuget'
+ object.nuget_metadatum
else
nil
end
diff --git a/app/graphql/types/packages/package_type_enum.rb b/app/graphql/types/packages/package_type_enum.rb
index e2b5cf3163e..17145d8e000 100644
--- a/app/graphql/types/packages/package_type_enum.rb
+++ b/app/graphql/types/packages/package_type_enum.rb
@@ -5,12 +5,13 @@ module Types
class PackageTypeEnum < BaseEnum
PACKAGE_TYPE_NAMES = {
pypi: 'PyPI',
- npm: 'npm'
+ npm: 'npm',
+ terraform_module: 'Terraform Module'
}.freeze
::Packages::Package.package_types.keys.each do |package_type|
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
- value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s
+ value package_type.to_s.upcase, description: "Packages from the #{type_name} package manager", value: package_type.to_s
end
end
end
diff --git a/app/graphql/types/permission_types/ci/job.rb b/app/graphql/types/permission_types/ci/job.rb
new file mode 100644
index 00000000000..c9a85317e67
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/job.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ module Ci
+ class Job < BasePermissionType
+ graphql_name 'JobPermissions'
+
+ abilities :read_job_artifacts, :read_build
+ ability_field :update_build, calls_gitaly: true
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index 5747e63d195..f6a5563d367 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -27,3 +27,5 @@ module Types
end
end
end
+
+::Types::PermissionTypes::Project.prepend_mod_with('Types::PermissionTypes::Project')
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 21534f40499..a2852588e89 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -30,8 +30,12 @@ module Types
markdown_field :description_html, null: true
field :tag_list, GraphQL::STRING_TYPE, null: true,
+ deprecated: { reason: 'Use `topics`', milestone: '13.12' },
description: 'List of project topics (not Git tags).'
+ field :topics, [GraphQL::STRING_TYPE], null: true,
+ description: 'List of project topics.'
+
field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true,
description: 'URL to connect to the project via SSH.'
field :http_url_to_repo, GraphQL::STRING_TYPE, null: true,
@@ -180,14 +184,15 @@ module Types
resolver: Resolvers::IssuesResolver.single
field :packages,
- description: 'Packages of the project.',
- resolver: Resolvers::ProjectPackagesResolver
+ description: 'Packages of the project.',
+ resolver: Resolvers::ProjectPackagesResolver
field :jobs,
- Types::Ci::JobType.connection_type,
- null: true,
- description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
- resolver: Resolvers::ProjectJobsResolver
+ type: Types::Ci::JobType.connection_type,
+ null: true,
+ authorize: :read_commit_status,
+ description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
+ resolver: Resolvers::ProjectJobsResolver
field :pipelines,
null: true,
@@ -337,6 +342,10 @@ module Types
description: 'Pipeline analytics.',
resolver: Resolvers::ProjectPipelineStatisticsResolver
+ field :ci_template, Types::Ci::TemplateType, null: true,
+ description: 'Find a single CI/CD template by name.',
+ resolver: Resolvers::Ci::TemplateResolver
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
@@ -386,4 +395,4 @@ module Types
end
end
-Types::ProjectType.prepend_if_ee('::EE::Types::ProjectType')
+Types::ProjectType.prepend_mod_with('Types::ProjectType')
diff --git a/app/graphql/types/projects/service_type_enum.rb b/app/graphql/types/projects/service_type_enum.rb
index fcb36fc233d..0a57cd48df4 100644
--- a/app/graphql/types/projects/service_type_enum.rb
+++ b/app/graphql/types/projects/service_type_enum.rb
@@ -5,7 +5,7 @@ module Types
class ServiceTypeEnum < BaseEnum
graphql_name 'ServiceType'
- ::Service.available_services_types(include_dev: false).each do |service_type|
+ ::Integration.available_services_types(include_dev: false).each do |service_type|
value service_type.underscore.upcase, value: service_type, description: "#{service_type} type"
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 8af0db644dd..8b7b9f0107b 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -79,8 +79,14 @@ module Types
field :issue, Types::IssueType,
null: true,
- description: 'Find an Issue.' do
- argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.'
+ description: 'Find an issue.' do
+ argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the issue.'
+ end
+
+ field :merge_request, Types::MergeRequestType,
+ null: true,
+ description: 'Find a merge request.' do
+ argument :id, ::Types::GlobalIDType[::MergeRequest], required: true, description: 'The global ID of the merge request.'
end
field :instance_statistics_measurements,
@@ -106,6 +112,19 @@ module Types
field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver
field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver
+ field :runner, Types::Ci::RunnerType,
+ null: true,
+ resolver: Resolvers::Ci::RunnerResolver,
+ extras: [:lookahead],
+ description: "Find a runner.",
+ feature_flag: :runner_graphql_query
+
+ field :runners, Types::Ci::RunnerType.connection_type,
+ null: true,
+ resolver: Resolvers::Ci::RunnersResolver,
+ description: "Find runners visible to the current user.",
+ feature_flag: :runner_graphql_query
+
field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
def design_management
@@ -119,6 +138,13 @@ module Types
GitlabSchema.find_by_gid(id)
end
+ def merge_request(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::MergeRequest].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+
def milestone(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
@@ -147,4 +173,4 @@ module Types
end
end
-Types::QueryType.prepend_if_ee('EE::Types::QueryType')
+Types::QueryType.prepend_mod_with('Types::QueryType')
diff --git a/app/graphql/types/release_assets_type.rb b/app/graphql/types/release_assets_type.rb
index 79c132358e0..d847d9842d5 100644
--- a/app/graphql/types/release_assets_type.rb
+++ b/app/graphql/types/release_assets_type.rb
@@ -13,7 +13,7 @@ module Types
field :count, GraphQL::INT_TYPE, null: true, method: :assets_count,
description: 'Number of assets of the release.'
- field :links, Types::ReleaseAssetLinkType.connection_type, null: true,
+ field :links, Types::ReleaseAssetLinkType.connection_type, null: true, method: :sorted_links,
description: 'Asset links of the release.'
field :sources, Types::ReleaseSourceType.connection_type, null: true,
description: 'Sources of the release.'
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index 912fc5f643a..8ed97d7e663 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -32,6 +32,66 @@ module Types
field :web_path, GraphQL::STRING_TYPE, null: true,
description: 'Web path of the blob.'
+ field :ide_edit_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to edit this blob in the Web IDE.'
+
+ field :fork_and_edit_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to edit this blob using a forked project.'
+
+ field :ide_fork_and_edit_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to edit this blob in the Web IDE using a forked project.'
+
+ field :size, GraphQL::INT_TYPE, null: true,
+ description: 'Size (in bytes) of the blob.'
+
+ field :raw_size, GraphQL::INT_TYPE, null: true,
+ description: 'Size (in bytes) of the blob, or the blob target if stored externally.'
+
+ field :raw_blob, GraphQL::STRING_TYPE, null: true, method: :data,
+ description: 'The raw content of the blob.'
+
+ field :raw_text_blob, GraphQL::STRING_TYPE, null: true, method: :text_only_data,
+ description: 'The raw content of the blob, if the blob is text data.'
+
+ field :stored_externally, GraphQL::BOOLEAN_TYPE, null: true, method: :stored_externally?,
+ description: "Whether the blob's content is stored externally (for instance, in LFS)."
+
+ field :edit_blob_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to edit the blob in the old-style editor.'
+
+ field :raw_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to download the raw blob.'
+
+ field :external_storage_url, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to download the raw blob via external storage, if enabled.'
+
+ field :replace_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path to replace the blob content.'
+
+ field :file_type, GraphQL::STRING_TYPE, null: true,
+ description: 'The expected format of the blob based on the extension.'
+
+ field :simple_viewer, type: Types::BlobViewerType,
+ description: 'Blob content simple viewer.',
+ null: false
+
+ field :rich_viewer, type: Types::BlobViewerType,
+ description: 'Blob content rich viewer.',
+ null: true
+
+ field :plain_data, GraphQL::STRING_TYPE,
+ description: 'Blob plain highlighted data.',
+ null: true,
+ calls_gitaly: true
+
+ field :can_modify_blob, GraphQL::BOOLEAN_TYPE, null: true, method: :can_modify_blob?,
+ calls_gitaly: true,
+ description: 'Whether the current user can modify the blob.'
+
+ def raw_text_blob
+ object.data unless object.binary?
+ end
+
def lfs_oid
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find
end
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 963a4296c4f..9d896888fa7 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -19,5 +19,9 @@ module Types
field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
complexity: 170, description: 'Names of branches available in this repository that match the search pattern.',
resolver: Resolvers::RepositoryBranchNamesResolver
+ field :disk_path, GraphQL::STRING_TYPE,
+ description: 'Shows a disk path of the repository.',
+ null: true,
+ authorize: :read_storage_disk_path
end
end
diff --git a/app/graphql/types/snippets/blob_viewer_type.rb b/app/graphql/types/snippets/blob_viewer_type.rb
index 9e77457c843..8b73234bbd9 100644
--- a/app/graphql/types/snippets/blob_viewer_type.rb
+++ b/app/graphql/types/snippets/blob_viewer_type.rb
@@ -2,48 +2,10 @@
module Types
module Snippets
- class BlobViewerType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
+ # Kept to avoid changing the type of existing fields. New fields should use
+ # ::Types::BlobViewerType directly
+ class BlobViewerType < ::Types::BlobViewerType # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'SnippetBlobViewer'
- description 'Represents how the blob content should be displayed'
-
- field :type, Types::BlobViewers::TypeEnum,
- description: 'Type of blob viewer.',
- null: false
-
- field :load_async, GraphQL::BOOLEAN_TYPE,
- description: 'Shows whether the blob content is loaded asynchronously.',
- null: false
-
- field :collapsed, GraphQL::BOOLEAN_TYPE,
- description: 'Shows whether the blob should be displayed collapsed.',
- method: :collapsed?,
- null: false
-
- field :too_large, GraphQL::BOOLEAN_TYPE,
- description: 'Shows whether the blob too large to be displayed.',
- method: :too_large?,
- null: false
-
- field :render_error, GraphQL::STRING_TYPE,
- description: 'Error rendering the blob content.',
- null: true
-
- field :file_type, GraphQL::STRING_TYPE,
- description: 'Content file type.',
- method: :partial_name,
- null: false
-
- field :loading_partial_name, GraphQL::STRING_TYPE,
- description: 'Loading partial name.',
- null: false
-
- def collapsed
- !!object&.collapsed?
- end
-
- def too_large
- !!object&.too_large?
- end
end
end
end
diff --git a/app/graphql/types/snippets/type_enum.rb b/app/graphql/types/snippets/type_enum.rb
index 243f05359db..0ddd73d278d 100644
--- a/app/graphql/types/snippets/type_enum.rb
+++ b/app/graphql/types/snippets/type_enum.rb
@@ -3,8 +3,8 @@
module Types
module Snippets
class TypeEnum < BaseEnum
- value 'personal', value: 'personal'
- value 'project', value: 'project'
+ value 'personal', description: 'Snippet created independent of any project.', value: 'personal'
+ value 'project', description: 'Snippet related to a specific project.', value: 'project'
end
end
end
diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb
new file mode 100644
index 00000000000..5356a998f0d
--- /dev/null
+++ b/app/graphql/types/subscription_type.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ class SubscriptionType < ::Types::BaseObject
+ graphql_name 'Subscription'
+
+ field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true,
+ description: 'Triggered when the assignees of an issuable are updated.'
+ end
+end
diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb
index 465e3c492bc..99a619f1b1d 100644
--- a/app/graphql/types/timelog_type.rb
+++ b/app/graphql/types/timelog_type.rb
@@ -26,6 +26,11 @@ module Types
null: true,
description: 'The issue that logged time was added to.'
+ field :merge_request,
+ Types::MergeRequestType,
+ null: true,
+ description: 'The merge request that logged time was added to.'
+
field :note,
Types::Notes::NoteType,
null: true,
@@ -38,5 +43,9 @@ module Types
def issue
Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.issue_id).find
end
+
+ def spent_at
+ object.spent_at || object.created_at
+ end
end
end
diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb
index ebf65e99936..ce61bc8a926 100644
--- a/app/graphql/types/todo_target_enum.rb
+++ b/app/graphql/types/todo_target_enum.rb
@@ -10,4 +10,4 @@ module Types
end
end
-Types::TodoTargetEnum.prepend_if_ee('::EE::Types::TodoTargetEnum')
+Types::TodoTargetEnum.prepend_mod_with('Types::TodoTargetEnum')
diff --git a/app/graphql/types/tree/type_enum.rb b/app/graphql/types/tree/type_enum.rb
index 6560d91e9e5..7acb83a2a8a 100644
--- a/app/graphql/types/tree/type_enum.rb
+++ b/app/graphql/types/tree/type_enum.rb
@@ -6,9 +6,9 @@ module Types
graphql_name 'EntryType'
description 'Type of a tree entry'
- value 'tree', value: :tree
- value 'blob', value: :blob
- value 'commit', value: :commit
+ value 'tree', description: 'Directory tree type.', value: :tree
+ value 'blob', description: 'File tree type.', value: :blob
+ value 'commit', description: 'Commit tree type.', value: :commit
end
end
end
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
new file mode 100644
index 00000000000..e5abc033155
--- /dev/null
+++ b/app/graphql/types/user_interface.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Types
+ module UserInterface
+ include Types::BaseInterface
+
+ graphql_name 'User'
+ description 'Representation of a GitLab user.'
+
+ field :user_permissions,
+ type: Types::PermissionTypes::User,
+ description: 'Permissions for the current user on the resource.',
+ null: false,
+ method: :itself
+
+ field :id,
+ type: GraphQL::ID_TYPE,
+ null: false,
+ description: 'ID of the user.'
+ field :bot,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: false,
+ description: 'Indicates if the user is a bot.',
+ method: :bot?
+ field :username,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'Username of the user. Unique within this instance of GitLab.'
+ field :name,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'Human-readable name of the user.'
+ field :state,
+ type: Types::UserStateEnum,
+ null: false,
+ description: 'State of the user.'
+ field :email,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: 'User email.', method: :public_email,
+ deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' }
+ field :public_email,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: "User's public email."
+ field :avatar_url,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: "URL of the user's avatar."
+ field :web_url,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'Web URL of the user.'
+ field :web_path,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'Web path of the user.'
+ field :todos,
+ resolver: Resolvers::TodoResolver,
+ description: 'To-do items of the user.'
+ field :group_memberships,
+ type: Types::GroupMemberType.connection_type,
+ null: true,
+ description: 'Group memberships of the user.'
+ field :group_count,
+ resolver: Resolvers::Users::GroupCountResolver,
+ description: 'Group count for the user.'
+ field :status,
+ type: Types::UserStatusType,
+ null: true,
+ description: 'User status.'
+ field :location,
+ type: ::GraphQL::STRING_TYPE,
+ null: true,
+ description: 'The location of the user.'
+ field :project_memberships,
+ type: Types::ProjectMemberType.connection_type,
+ null: true,
+ description: 'Project memberships of the user.'
+ field :starred_projects,
+ description: 'Projects starred by the user.',
+ resolver: Resolvers::UserStarredProjectsResolver
+
+ # Merge request field: MRs can be authored, assigned, or assigned-for-review:
+ field :authored_merge_requests,
+ resolver: Resolvers::AuthoredMergeRequestsResolver,
+ description: 'Merge requests authored by the user.'
+ field :assigned_merge_requests,
+ resolver: Resolvers::AssignedMergeRequestsResolver,
+ description: 'Merge requests assigned to the user.'
+ field :review_requested_merge_requests,
+ resolver: Resolvers::ReviewRequestedMergeRequestsResolver,
+ description: 'Merge requests assigned to the user for review.'
+
+ field :snippets,
+ description: 'Snippets authored by the user.',
+ resolver: Resolvers::Users::SnippetsResolver
+ field :callouts,
+ Types::UserCalloutType.connection_type,
+ null: true,
+ description: 'User callouts that belong to the user.'
+
+ definition_methods do
+ def resolve_type(object, context)
+ # in the absense of other information, we cannot tell - just default to
+ # the core user type.
+ ::Types::UserType
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/user_merge_request_interaction_type.rb b/app/graphql/types/user_merge_request_interaction_type.rb
index 5ff0d79f13e..b9ff489e0d6 100644
--- a/app/graphql/types/user_merge_request_interaction_type.rb
+++ b/app/graphql/types/user_merge_request_interaction_type.rb
@@ -44,4 +44,4 @@ module Types
end
end
-::Types::UserMergeRequestInteractionType.prepend_if_ee('EE::Types::UserMergeRequestInteractionType')
+::Types::UserMergeRequestInteractionType.prepend_mod_with('Types::UserMergeRequestInteractionType')
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 3d7db80ae11..a6f5b7e7456 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -1,102 +1,13 @@
# frozen_string_literal: true
module Types
- class UserType < BaseObject
- graphql_name 'User'
- description 'Representation of a GitLab user.'
+ class UserType < ::Types::BaseObject
+ graphql_name 'UserCore'
+ description 'Core represention of a GitLab user.'
+ implements ::Types::UserInterface
authorize :read_user
present_using UserPresenter
-
- expose_permissions Types::PermissionTypes::User
-
- field :id,
- type: GraphQL::ID_TYPE,
- null: false,
- description: 'ID of the user.'
- field :bot,
- type: GraphQL::BOOLEAN_TYPE,
- null: false,
- description: 'Indicates if the user is a bot.',
- method: :bot?
- field :username,
- type: GraphQL::STRING_TYPE,
- null: false,
- description: 'Username of the user. Unique within this instance of GitLab.'
- field :name,
- type: GraphQL::STRING_TYPE,
- null: false,
- description: 'Human-readable name of the user.'
- field :state,
- type: Types::UserStateEnum,
- null: false,
- description: 'State of the user.'
- field :email,
- type: GraphQL::STRING_TYPE,
- null: true,
- description: 'User email.', method: :public_email,
- deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' }
- field :public_email,
- type: GraphQL::STRING_TYPE,
- null: true,
- description: "User's public email."
- field :avatar_url,
- type: GraphQL::STRING_TYPE,
- null: true,
- description: "URL of the user's avatar."
- field :web_url,
- type: GraphQL::STRING_TYPE,
- null: false,
- description: 'Web URL of the user.'
- field :web_path,
- type: GraphQL::STRING_TYPE,
- null: false,
- description: 'Web path of the user.'
- field :todos,
- resolver: Resolvers::TodoResolver,
- description: 'To-do items of the user.'
- field :group_memberships,
- type: Types::GroupMemberType.connection_type,
- null: true,
- description: 'Group memberships of the user.'
- field :group_count,
- resolver: Resolvers::Users::GroupCountResolver,
- description: 'Group count for the user.',
- feature_flag: :user_group_counts
- field :status,
- type: Types::UserStatusType,
- null: true,
- description: 'User status.'
- field :location,
- type: ::GraphQL::STRING_TYPE,
- null: true,
- description: 'The location of the user.'
- field :project_memberships,
- type: Types::ProjectMemberType.connection_type,
- null: true,
- description: 'Project memberships of the user.'
- field :starred_projects,
- description: 'Projects starred by the user.',
- resolver: Resolvers::UserStarredProjectsResolver
-
- # Merge request field: MRs can be authored, assigned, or assigned-for-review:
- field :authored_merge_requests,
- resolver: Resolvers::AuthoredMergeRequestsResolver,
- description: 'Merge requests authored by the user.'
- field :assigned_merge_requests,
- resolver: Resolvers::AssignedMergeRequestsResolver,
- description: 'Merge requests assigned to the user.'
- field :review_requested_merge_requests,
- resolver: Resolvers::ReviewRequestedMergeRequestsResolver,
- description: 'Merge requests assigned to the user for review.'
-
- field :snippets,
- description: 'Snippets authored by the user.',
- resolver: Resolvers::Users::SnippetsResolver
- field :callouts,
- Types::UserCalloutType.connection_type,
- null: true,
- description: 'User callouts that belong to the user.'
end
end
diff --git a/app/helpers/analytics/navbar_helper.rb b/app/helpers/analytics/navbar_helper.rb
index 33a5028cdf1..091571ff15a 100644
--- a/app/helpers/analytics/navbar_helper.rb
+++ b/app/helpers/analytics/navbar_helper.rb
@@ -13,14 +13,6 @@ module Analytics
end
end
- def project_analytics_navbar_links(project, current_user)
- [
- cycle_analytics_navbar_link(project, current_user),
- repository_analytics_navbar_link(project, current_user),
- ci_cd_analytics_navbar_link(project, current_user)
- ].compact
- end
-
def group_analytics_navbar_links(group, current_user)
[]
end
@@ -30,40 +22,7 @@ module Analytics
def navbar_sub_item(args)
NavbarSubItem.new(**args)
end
-
- def cycle_analytics_navbar_link(project, current_user)
- return unless project_nav_tab?(:cycle_analytics)
-
- navbar_sub_item(
- title: _('Value Stream'),
- path: 'cycle_analytics#show',
- link: project_cycle_analytics_path(project),
- link_to_options: { class: 'shortcuts-project-cycle-analytics' }
- )
- end
-
- def repository_analytics_navbar_link(project, current_user)
- return if project.empty_repo?
-
- navbar_sub_item(
- title: _('Repository'),
- path: 'graphs#charts',
- link: charts_project_graph_path(project, current_ref),
- link_to_options: { class: 'shortcuts-repository-charts' }
- )
- end
-
- def ci_cd_analytics_navbar_link(project, current_user)
- return unless project_nav_tab?(:pipelines)
- return unless project.feature_available?(:builds, current_user) || !project.empty_repo?
-
- navbar_sub_item(
- title: _('CI/CD'),
- path: 'pipelines#charts',
- link: charts_project_pipelines_path(project)
- )
- end
end
end
-Analytics::NavbarHelper.prepend_if_ee('EE::Analytics::NavbarHelper')
+Analytics::NavbarHelper.prepend_mod_with('Analytics::NavbarHelper')
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 65feea4f6e0..60e37c96f61 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -83,5 +83,4 @@ module AppearancesHelper
end
end
-AppearancesHelper.prepend_if_ee('EE::AppearancesHelper')
-AppearancesHelper.prepend_if_jh('JH::AppearancesHelper')
+AppearancesHelper.prepend_mod
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a2ef2f1207c..2e15b3f22c2 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
else
'Never'
end
- rescue
+ rescue StandardError
'Never'
end
@@ -382,15 +382,26 @@ module ApplicationHelper
def autocomplete_data_sources(object, noteable_type)
return {} unless object && noteable_type
- {
- members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
- issues: issues_project_autocomplete_sources_path(object),
- mergeRequests: merge_requests_project_autocomplete_sources_path(object),
- labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
- milestones: milestones_project_autocomplete_sources_path(object),
- commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
- snippets: snippets_project_autocomplete_sources_path(object)
- }
+ if object.is_a?(Group)
+ {
+ members: members_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ issues: issues_group_autocomplete_sources_path(object),
+ mergeRequests: merge_requests_group_autocomplete_sources_path(object),
+ labels: labels_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ milestones: milestones_group_autocomplete_sources_path(object),
+ commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
+ }
+ else
+ {
+ members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ issues: issues_project_autocomplete_sources_path(object),
+ mergeRequests: merge_requests_project_autocomplete_sources_path(object),
+ labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ milestones: milestones_project_autocomplete_sources_path(object),
+ commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ snippets: snippets_project_autocomplete_sources_path(object)
+ }
+ end
end
def asset_to_string(name)
@@ -409,5 +420,4 @@ module ApplicationHelper
end
end
-ApplicationHelper.prepend_if_ee('EE::ApplicationHelper')
-ApplicationHelper.prepend_if_jh('JH::ApplicationHelper')
+ApplicationHelper.prepend_mod
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 504ebb5606e..0e3dff27da9 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -233,6 +233,7 @@ module ApplicationSettingsHelper
:external_pipeline_validation_service_token,
:external_pipeline_validation_service_url,
:first_day_of_week,
+ :floc_enabled,
:force_pages_access_control,
:gitaly_timeout_default,
:gitaly_timeout_medium,
@@ -302,6 +303,7 @@ module ApplicationSettingsHelper
:sourcegraph_public_only,
:spam_check_endpoint_enabled,
:spam_check_endpoint_url,
+ :spam_check_api_key,
:terminal_max_session_time,
:terms,
:throttle_authenticated_api_enabled,
@@ -310,9 +312,15 @@ module ApplicationSettingsHelper
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_web_requests_per_period,
+ :throttle_authenticated_packages_api_enabled,
+ :throttle_authenticated_packages_api_period_in_seconds,
+ :throttle_authenticated_packages_api_requests_per_period,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_period_in_seconds,
:throttle_unauthenticated_requests_per_period,
+ :throttle_unauthenticated_packages_api_enabled,
+ :throttle_unauthenticated_packages_api_period_in_seconds,
+ :throttle_unauthenticated_packages_api_requests_per_period,
:throttle_protected_paths_enabled,
:throttle_protected_paths_period_in_seconds,
:throttle_protected_paths_requests_per_period,
@@ -358,7 +366,8 @@ module ApplicationSettingsHelper
:rate_limiting_response_text,
:container_registry_expiration_policies_worker_capacity,
:container_registry_cleanup_tags_service_max_list_size,
- :keep_latest_artifact
+ :keep_latest_artifact,
+ :whats_new_variant
]
end
@@ -387,7 +396,7 @@ module ApplicationSettingsHelper
end
def integration_expanded?(substring)
- @application_setting.errors.any? { |k| k.to_s.start_with?(substring) }
+ @application_setting.errors.messages.any? { |k, _| k.to_s.start_with?(substring) }
end
def instance_clusters_enabled?
@@ -429,8 +438,8 @@ module ApplicationSettingsHelper
end
end
-ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
+ApplicationSettingsHelper.prepend_mod_with('ApplicationSettingsHelper')
# The methods in `EE::ApplicationSettingsHelper` should be available as both
# instance and class methods.
-ApplicationSettingsHelper.extend_if_ee('EE::ApplicationSettingsHelper')
+ApplicationSettingsHelper.extend_mod_with('ApplicationSettingsHelper')
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cacf9c7ad0b..a0c3a6f2f52 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -16,7 +16,7 @@ module AuthHelper
twitter
).freeze
LDAP_PROVIDER = /\Aldap/.freeze
- TRIAL_REGISTRATION_PROVIDERS = %w(google_oauth2 github).freeze
+ POPULAR_PROVIDERS = %w(google_oauth2 github).freeze
def ldap_enabled?
Gitlab::Auth::Ldap::Config.enabled?
@@ -116,19 +116,12 @@ module AuthHelper
providers = button_based_providers.map(&:to_s) - disabled_providers
providers.sort_by do |provider|
- case provider
- when 'google_oauth2'
- 0
- when 'github'
- 1
- else
- 2
- end
+ POPULAR_PROVIDERS.index(provider) || POPULAR_PROVIDERS.length
end
end
- def trial_enabled_button_based_providers
- enabled_button_based_providers & TRIAL_REGISTRATION_PROVIDERS
+ def popular_enabled_button_based_providers
+ enabled_button_based_providers & POPULAR_PROVIDERS
end
def button_based_providers_enabled?
@@ -176,11 +169,23 @@ module AuthHelper
!current_user
end
+ def auth_app_owner_text(owner)
+ return unless owner
+
+ if owner.is_a?(Group)
+ group_link = link_to(owner.name, group_path(owner))
+ _("This application was created for group %{group_link}.").html_safe % { group_link: group_link }
+ else
+ user_link = link_to(owner.name, user_path(owner))
+ _("This application was created by %{user_link}.").html_safe % { user_link: user_link }
+ end
+ end
+
extend self
end
-AuthHelper.prepend_if_ee('EE::AuthHelper')
+AuthHelper.prepend_mod_with('AuthHelper')
# The methods added in EE should be available as both class and instance
# methods, just like the methods provided by `AuthHelper` itself.
-AuthHelper.extend_if_ee('EE::AuthHelper')
+AuthHelper.extend_mod_with('AuthHelper')
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 09f91f350bd..4cfa1528d9b 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -98,6 +98,14 @@ module AvatarsHelper
end
end
+ def avatar_without_link(resource, options = {})
+ if resource.is_a?(User)
+ user_avatar_without_link(options.merge(user: resource))
+ elsif resource.is_a?(Group)
+ group_icon(resource, options.merge(class: 'avatar'))
+ end
+ end
+
private
def avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path:)
@@ -136,11 +144,12 @@ module AvatarsHelper
def source_identicon(source, options = {})
bg_key = (source.id % 7) + 1
+ size_class = "s#{options[:size]}" if options[:size]
options[:class] =
- [*options[:class], "identicon bg#{bg_key}"].join(' ')
+ [*options[:class], "identicon bg#{bg_key}", size_class].compact.join(' ')
- content_tag(:div, class: options[:class].strip) do
+ content_tag(:span, class: options[:class].strip) do
source.name[0, 1].upcase
end
end
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index af9ab93d459..196415bb363 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -17,4 +17,4 @@ module AwardEmojiHelper
end
end
-AwardEmojiHelper.prepend_if_ee('EE::AwardEmojiHelper')
+AwardEmojiHelper.prepend_mod_with('AwardEmojiHelper')
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 3144686bba9..dfd6de3f1d5 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -206,10 +206,6 @@ module BlobHelper
@gitlab_ci_ymls ||= TemplateFinder.all_template_names(project, :gitlab_ci_ymls)
end
- def gitlab_ci_syntax_ymls(project)
- @gitlab_ci_syntax_ymls ||= TemplateFinder.all_template_names(project, :gitlab_ci_syntax_ymls)
- end
-
def metrics_dashboard_ymls(project)
@metrics_dashboard_ymls ||= TemplateFinder.all_template_names(project, :metrics_dashboard_ymls)
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 49963d14934..f72f8bfd151 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -10,7 +10,7 @@ module BoardsHelper
boards_endpoint: @boards_endpoint,
lists_endpoint: board_lists_path(board),
board_id: board.id,
- disabled: (!can?(current_user, :create_non_backlog_issues, board)).to_s,
+ disabled: board.disabled_for?(current_user).to_s,
root_path: root_path,
full_path: full_path,
bulk_update_path: @bulk_issues_path,
@@ -89,6 +89,10 @@ module BoardsHelper
@current_board_parent ||= @group || @project
end
+ def current_board_namespace
+ @current_board_namespace = board.group_board? ? @group : @project.namespace
+ end
+
def can_update?
can?(current_user, :admin_issue, board)
end
@@ -136,4 +140,4 @@ module BoardsHelper
end
end
-BoardsHelper.prepend_if_ee('EE::BoardsHelper')
+BoardsHelper.prepend_mod_with('BoardsHelper')
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 8f87cd5bfe0..a500a695029 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -22,4 +22,4 @@ module BranchesHelper
end
end
-BranchesHelper.prepend_if_ee('EE::BranchesHelper')
+BranchesHelper.prepend_mod_with('BranchesHelper')
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 1b00f583b55..27d6ee57d8b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -100,4 +100,4 @@ module ButtonHelper
end
end
-ButtonHelper.prepend_if_ee('EE::ButtonHelper')
+ButtonHelper.prepend_mod_with('ButtonHelper')
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index a0d169c1358..23f2a082a68 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -15,7 +15,8 @@ module Ci
"build_stage" => @build.stage,
"log_state" => '',
"build_options" => javascript_build_options,
- "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs')
+ "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs'),
+ "code_quality_help_url" => help_page_path('user/project/merge_requests/code_quality', anchor: 'troubleshooting')
}
end
@@ -36,4 +37,4 @@ module Ci
end
end
-Ci::JobsHelper.prepend_if_ee('::EE::Ci::JobsHelper')
+Ci::JobsHelper.prepend_mod_with('Ci::JobsHelper')
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index ceb18d90c92..8c8ee2d4d0f 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -9,21 +9,29 @@ module Ci
end
def js_pipeline_editor_data(project)
+ commit_sha = project.commit ? project.commit.sha : ''
{
"ci-config-path": project.ci_config_path_or_default,
- "commit-sha" => project.commit ? project.commit.sha : '',
+ "ci-examples-help-page-path" => help_page_path('ci/examples/README'),
+ "ci-help-page-path" => help_page_path('ci/README'),
+ "commit-sha" => commit_sha,
"default-branch" => project.default_branch,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name": params[:branch_name],
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
+ "pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
+ "pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
+ "runner-help-page-path" => help_page_path('ci/runners/README'),
+ "total-branches" => project.repository.branches.length,
"yml-help-page-path" => help_page_path('ci/yaml/README')
}
end
end
end
-Ci::PipelineEditorHelper.prepend_if_ee('EE::Ci::PipelineEditorHelper')
+Ci::PipelineEditorHelper.prepend_mod_with('Ci::PipelineEditorHelper')
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index cabb43f45fd..f42cd53ae3a 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -50,10 +50,9 @@ module Ci
{ name: 'Gradle', logo: image_path('illustrations/logos/gradle.svg') },
{ name: 'Grails', logo: image_path('illustrations/logos/grails.svg') },
{ name: 'dotNET', logo: image_path('illustrations/logos/dotnet.svg') },
- { name: 'Rails', logo: image_path('illustrations/logos/rails.svg') },
{ name: 'Julia', logo: image_path('illustrations/logos/julia.svg') },
{ name: 'Laravel', logo: image_path('illustrations/logos/laravel.svg') },
- { name: 'Latex', logo: image_path('illustrations/logos/latex.svg') },
+ { name: 'LaTeX', logo: image_path('illustrations/logos/latex.svg') },
{ name: 'Maven', logo: image_path('illustrations/logos/maven.svg') },
{ name: 'Mono', logo: image_path('illustrations/logos/mono.svg') },
{ name: 'Nodejs', logo: image_path('illustrations/logos/node_js.svg') },
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 82347053d6f..550fa4de2c5 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -75,4 +75,4 @@ module Ci
end
end
-Ci::RunnersHelper.prepend_if_ee('EE::Ci::RunnersHelper')
+Ci::RunnersHelper.prepend_mod_with('Ci::RunnersHelper')
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index e7a81eb5629..9b952ad127e 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -128,7 +128,7 @@ module CommitsHelper
%w(btn gpg-status-box) + Array(additional_classes)
end
- def conditionally_paginate_diff_files(diffs, paginate:, per: Projects::CommitController::COMMIT_DIFFS_PER_PAGE)
+ def conditionally_paginate_diff_files(diffs, paginate:, per:)
if paginate
Kaminari.paginate_array(diffs.diff_files.to_a).page(params[:page]).per(per)
else
@@ -148,6 +148,27 @@ module CommitsHelper
end
end
+ # This is used to calculate a cache key for the app/views/projects/commits/_commit.html.haml
+ # partial. It takes some of the same parameters as used in the partial and will hash the
+ # current pipeline status.
+ #
+ # This includes a keyed hash for values that can be nil, to prevent invalid cache entries
+ # being served if the order should change in future.
+ def commit_partial_cache_key(commit, ref:, merge_request:, request:)
+ [
+ commit,
+ commit.author,
+ ref,
+ {
+ merge_request: merge_request,
+ pipeline_status: commit.status_for(ref),
+ xhr: request.xhr?,
+ controller: controller.controller_path,
+ path: @path # referred to in #link_to_browse_code
+ }
+ ]
+ end
+
protected
# Private: Returns a link to a person. If the person has a matching user and
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 08f357916b5..95bbf2eff41 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -66,4 +66,4 @@ module DashboardHelper
end
end
-DashboardHelper.prepend_if_ee('EE::DashboardHelper')
+DashboardHelper.prepend_mod_with('DashboardHelper')
diff --git a/app/helpers/dev_ops_report_helper.rb b/app/helpers/dev_ops_report_helper.rb
index ab7e56fc1a2..c2200a4c3da 100644
--- a/app/helpers/dev_ops_report_helper.rb
+++ b/app/helpers/dev_ops_report_helper.rb
@@ -1,18 +1,80 @@
# frozen_string_literal: true
module DevOpsReportHelper
+ def devops_score_metrics(metric)
+ return {} if metric.blank?
+
+ {
+ averageScore: average_score_data(metric),
+ cards: devops_score_card_data(metric),
+ createdAt: metric.created_at.strftime('%Y-%m-%d %H:%M')
+ }
+ end
+
+ private
+
+ def format_score(score)
+ precision = score < 1 ? 2 : 1
+ number_with_precision(score, precision: precision)
+ end
+
def score_level(score)
if score < 33.33
- 'low'
+ {
+ label: s_('DevopsReport|Low'),
+ variant: 'muted'
+ }
elsif score < 66.66
- 'average'
+ {
+ label: s_('DevopsReport|Moderate'),
+ variant: 'neutral'
+ }
else
- 'high'
+ {
+ label: s_('DevopsReport|High'),
+ variant: 'success'
+ }
end
end
- def format_score(score)
- precision = score < 1 ? 2 : 1
- number_with_precision(score, precision: precision)
+ def average_score_level(score)
+ if score < 33.33
+ {
+ label: s_('DevopsReport|Low'),
+ variant: 'danger',
+ icon: 'status-failed'
+ }
+ elsif score < 66.66
+ {
+ label: s_('DevopsReport|Moderate'),
+ variant: 'warning',
+ icon: 'status-alert'
+ }
+ else
+ {
+ label: s_('DevopsReport|High'),
+ variant: 'success',
+ icon: 'status_success_solid'
+ }
+ end
+ end
+
+ def average_score_data(metric)
+ {
+ value: format_score(metric.average_percentage_score),
+ scoreLevel: average_score_level(metric.average_percentage_score)
+ }
+ end
+
+ def devops_score_card_data(metric)
+ metric.cards.map do |card|
+ {
+ title: "#{card.title} #{card.description}",
+ usage: format_score(card.instance_score),
+ leadInstance: format_score(card.leader_score),
+ score: format_score(card.percentage_score),
+ scoreLevel: score_level(card.percentage_score)
+ }
+ end
end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 7bf3cb6230b..e430b0f402b 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -23,14 +23,16 @@ module DiffHelper
end
end
+ def show_only_context_commits?
+ !!params[:only_context_commits] || @merge_request&.commits&.empty?
+ end
+
def diff_options
options = { ignore_whitespace_change: hide_whitespace?, expanded: diffs_expanded? }
if action_name == 'diff_for_path'
options[:expanded] = true
options[:paths] = params.values_at(:old_path, :new_path)
- elsif action_name == 'show'
- options[:include_context_commits] = true unless @project.context_commits_enabled?
end
options
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index b58ff21b257..0b1bdb68e50 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -298,4 +298,4 @@ module EmailsHelper
end
end
-EmailsHelper.prepend_if_ee('EE::EmailsHelper')
+EmailsHelper.prepend_mod_with('EmailsHelper')
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 7f0c59f65a0..594c6fedef1 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -34,7 +34,7 @@ module EnvironmentsHelper
def environment_logs_data(project, environment)
{
"environment_name": environment.name,
- "environments_path": project_environments_path(project, format: :json),
+ "environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
"cluster_applications_documentation_path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
"clusters_path": project_clusters_path(project, format: :json)
@@ -62,7 +62,8 @@ module EnvironmentsHelper
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
'custom_metrics_available' => "#{custom_metrics_available?(project)}",
'prometheus_alerts_available' => "#{can?(current_user, :read_prometheus_alerts, project)}",
- 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
+ 'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase,
+ 'has_managed_prometheus' => has_managed_prometheus?(project).to_s
}
end
@@ -78,6 +79,10 @@ module EnvironmentsHelper
}
end
+ def has_managed_prometheus?(project)
+ project.prometheus_service&.prometheus_available? == true
+ end
+
def metrics_dashboard_base_path(environment, project)
# This is needed to support our transition from environment scoped metric paths to project scoped.
if project
@@ -117,4 +122,4 @@ module EnvironmentsHelper
end
end
-EnvironmentsHelper.prepend_if_ee('::EE::EnvironmentsHelper')
+EnvironmentsHelper.prepend_mod_with('EnvironmentsHelper')
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 6b3abb4274e..03c3ee3363d 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -228,7 +228,7 @@ module EventsHelper
def event_commit_title(message)
message ||= ''
(message.split("\n").first || "").truncate(70)
- rescue
+ rescue StandardError
"--broken encoding"
end
@@ -290,4 +290,4 @@ module EventsHelper
end
end
-EventsHelper.prepend_if_ee('EE::EventsHelper')
+EventsHelper.prepend_mod_with('EventsHelper')
diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb
index 38a4f7f1b4b..92d06471384 100644
--- a/app/helpers/export_helper.rb
+++ b/app/helpers/export_helper.rb
@@ -25,4 +25,4 @@ module ExportHelper
end
end
-ExportHelper.prepend_if_ee('EE::ExportHelper')
+ExportHelper.prepend_mod_with('ExportHelper')
diff --git a/app/helpers/feature_flags_helper.rb b/app/helpers/feature_flags_helper.rb
index e50191a471f..2b8804bc07e 100644
--- a/app/helpers/feature_flags_helper.rb
+++ b/app/helpers/feature_flags_helper.rb
@@ -16,4 +16,4 @@ module FeatureFlagsHelper
end
end
-FeatureFlagsHelper.prepend_if_ee('::EE::FeatureFlagsHelper')
+FeatureFlagsHelper.prepend_mod_with('FeatureFlagsHelper')
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index d0276c91316..cf3e99eee49 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -131,4 +131,4 @@ module FormHelper
end
end
-FormHelper.prepend_if_ee('::EE::FormHelper')
+FormHelper.prepend_mod_with('FormHelper')
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 48af4793fb0..0a684d92eb1 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -166,6 +166,16 @@ module GitlabRoutingHelper
resend_invite_group_group_member_path(group_member.source, group_member)
end
+ # Members
+ def source_members_url(member)
+ case member.source_type
+ when 'Namespace'
+ group_group_members_url(member.source)
+ when 'Project'
+ project_project_members_url(member.source)
+ end
+ end
+
# Artifacts
# Rails path generators are slow because they need to do large regex comparisons
@@ -354,6 +364,10 @@ module GitlabRoutingHelper
[api_graphql_path, "pipelines/id/#{pipeline.id}"].join(':')
end
+ def graphql_etag_pipeline_sha_path(sha)
+ [api_graphql_path, "pipelines/sha/#{sha}"].join(':')
+ end
+
private
def snippet_query_params(snippet, *args)
@@ -370,4 +384,4 @@ module GitlabRoutingHelper
end
end
-GitlabRoutingHelper.include_if_ee('EE::GitlabRoutingHelper')
+GitlabRoutingHelper.include_mod_with('GitlabRoutingHelper')
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index bcbc67957eb..3a94f7d47c2 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -28,4 +28,4 @@ module GraphHelper
end
end
-GraphHelper.prepend_if_ee('EE::GraphHelper')
+GraphHelper.prepend_mod_with('GraphHelper')
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index 3e7d6febabf..79191616c8f 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -13,31 +13,45 @@ module Groups::GroupMembersHelper
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level
end
- def group_group_links_data_json(group_links)
- GroupLink::GroupGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json
+ def group_members_list_data_json(group, members, pagination = {})
+ group_members_list_data(group, members, pagination).to_json
end
- def members_data_json(group, members)
- MemberSerializer.new.represent(members, { current_user: current_user, group: group, source: group }).to_json
+ def group_group_links_list_data_json(group)
+ group_group_links_list_data(group).to_json
+ end
+
+ private
+
+ def group_members_serialized(group, members)
+ MemberSerializer.new.represent(members, { current_user: current_user, group: group, source: group })
+ end
+
+ def group_group_links_serialized(group_links)
+ GroupLink::GroupGroupLinkSerializer.new.represent(group_links, { current_user: current_user })
end
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
- def group_members_list_data_attributes(group, members)
+ def group_members_list_data(group, members, pagination)
{
- members: members_data_json(group, members),
+ members: group_members_serialized(group, members),
+ pagination: members_pagination_data(members, pagination),
member_path: group_group_member_path(group, ':id'),
source_id: group.id,
- can_manage_members: can?(current_user, :admin_group_member, group).to_s
+ can_manage_members: can?(current_user, :admin_group_member, group)
}
end
- def group_group_links_list_data_attributes(group)
+ def group_group_links_list_data(group)
+ group_links = group.shared_with_group_links
+
{
- members: group_group_links_data_json(group.shared_with_group_links),
+ members: group_group_links_serialized(group_links),
+ pagination: members_pagination_data(group_links),
member_path: group_group_link_path(group, ':id'),
source_id: group.id
}
end
end
-Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
+Groups::GroupMembersHelper.prepend_mod_with('Groups::GroupMembersHelper')
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 5ce23baa226..8f647a49a64 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -7,7 +7,11 @@ module GroupsHelper
groups#details
groups#activity
groups#subgroups
- ]
+ ].tap do |paths|
+ break paths if Feature.disabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
+
+ paths.concat(['labels#index', 'group_members#index'])
+ end
end
def group_settings_nav_link_paths
@@ -25,7 +29,9 @@ module GroupsHelper
applications#index
applications#show
applications#edit
- packages_and_registries#index
+ packages_and_registries#show
+ groups/runners#show
+ groups/runners#edit
]
end
@@ -36,6 +42,14 @@ module GroupsHelper
]
end
+ def group_information_title(group)
+ if Feature.enabled?(:sidebar_refactor, current_user)
+ group.subgroup? ? _('Subgroup information') : _('Group information')
+ else
+ group.subgroup? ? _('Subgroup overview') : _('Group overview')
+ end
+ end
+
def group_container_registry_nav?
Gitlab.config.registry.enabled &&
can?(current_user, :read_container_image, @group)
@@ -113,9 +127,7 @@ module GroupsHelper
@has_group_title = true
full_title = []
- ancestors = group.ancestors.with_route
-
- ancestors.reverse_each.with_index do |parent, index|
+ sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
@@ -141,9 +153,9 @@ module GroupsHelper
def projects_lfs_status(group)
lfs_status =
if group.lfs_enabled?
- group.projects.select(&:lfs_enabled?).size
+ group.projects.count(&:lfs_enabled?)
else
- group.projects.reject(&:lfs_enabled?).size
+ group.projects.count { |project| !project.lfs_enabled? }
end
size = group.projects.size
@@ -206,10 +218,9 @@ module GroupsHelper
end
def show_invite_banner?(group)
- Feature.enabled?(:invite_your_teammates_banner_a, group) &&
- can?(current_user, :admin_group, group) &&
- !just_created? &&
- !multiple_members?(group)
+ can?(current_user, :admin_group, group) &&
+ !just_created? &&
+ !multiple_members?(group)
end
def render_setting_to_allow_project_access_token_creation?(group)
@@ -231,7 +242,7 @@ module GroupsHelper
end
def multiple_members?(group)
- group.member_count > 1
+ group.member_count > 1 || group.members_with_parents.count > 1
end
def get_group_sidebar_links
@@ -285,11 +296,20 @@ module GroupsHelper
end
def oldest_consecutively_locked_ancestor(group)
- group.ancestors.find do |group|
+ sorted_ancestors(group).find do |group|
!group.has_parent? || !group.parent.share_with_group_lock?
end
end
+ # Ancestors sorted by hierarchy depth in bottom-top order.
+ def sorted_ancestors(group)
+ if group.root_ancestor.use_traversal_ids?
+ group.ancestors(hierarchy_order: :asc)
+ else
+ group.ancestors
+ end
+ end
+
def default_help
s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.")
end
@@ -327,4 +347,4 @@ module GroupsHelper
end
end
-GroupsHelper.prepend_if_ee('EE::GroupsHelper')
+GroupsHelper.prepend_mod_with('GroupsHelper')
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 9466a37ed93..2725d28c47c 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -38,4 +38,4 @@ module HooksHelper
end
end
-HooksHelper.prepend_if_ee('EE::HooksHelper')
+HooksHelper.prepend_mod_with('HooksHelper')
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 61d8d0f779d..d1c84bd4141 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -17,7 +17,8 @@ module IdeHelper
'file-path' => @path,
'merge-request' => @merge_request,
'fork-info' => @fork_info&.to_json,
- 'project' => convert_to_project_entity_json(@project)
+ 'project' => convert_to_project_entity_json(@project),
+ 'enable-environments-guidance' => enable_environments_guidance?.to_s
}
end
@@ -28,6 +29,18 @@ module IdeHelper
API::Entities::Project.represent(project).to_json
end
+
+ def enable_environments_guidance?
+ experiment(:in_product_guidance_environments_webide, project: @project) do |e|
+ e.try { !has_dismissed_ide_environments_callout? }
+
+ e.run
+ end
+ end
+
+ def has_dismissed_ide_environments_callout?
+ current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance')
+ end
end
-::IdeHelper.prepend_if_ee('::EE::IdeHelper')
+::IdeHelper.prepend_mod_with('IdeHelper')
diff --git a/app/helpers/in_product_marketing_helper.rb b/app/helpers/in_product_marketing_helper.rb
index 9e59a04d709..09546f251f9 100644
--- a/app/helpers/in_product_marketing_helper.rb
+++ b/app/helpers/in_product_marketing_helper.rb
@@ -1,381 +1,12 @@
# frozen_string_literal: true
module InProductMarketingHelper
- def subject_line(track, series)
- {
- create: [
- s_('InProductMarketing|Create a project in GitLab in 5 minutes'),
- s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'),
- s_('InProductMarketing|Understand repository mirroring')
- ],
- verify: [
- s_('InProductMarketing|Feel the need for speed?'),
- s_('InProductMarketing|3 ways to dive into GitLab CI/CD'),
- s_('InProductMarketing|Explore the power of GitLab CI/CD')
- ],
- trial: [
- s_('InProductMarketing|Go farther with GitLab'),
- s_('InProductMarketing|Automated security scans directly within GitLab'),
- s_('InProductMarketing|Take your source code management to the next level')
- ],
- team: [
- s_('InProductMarketing|Working in GitLab = more efficient'),
- s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"),
- s_('InProductMarketing|Your teams can be more efficient')
- ]
- }[track][series]
- end
-
- def in_product_marketing_logo(track, series)
- inline_image_link('mailers/in_product_marketing', "#{track}-#{series}.png", { width: '150', style: 'width: 150px;' })
- end
-
- def about_link(folder, image, width)
- link_to inline_image_link(folder, image, { width: width, style: "width: #{width}px;", alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/'
- end
-
- def in_product_marketing_tagline(track, series)
- {
- create: [
- s_('InProductMarketing|Get started today'),
- s_('InProductMarketing|Get our import guides'),
- s_('InProductMarketing|Need an alternative to importing?')
- ],
- verify: [
- s_('InProductMarketing|Use GitLab CI/CD'),
- s_('InProductMarketing|Test, create, deploy'),
- s_('InProductMarketing|Are your runners ready?')
- ],
- trial: [
- s_('InProductMarketing|Start a free trial of GitLab Ultimate – no CC required'),
- s_('InProductMarketing|Improve app security with a 30-day trial'),
- s_('InProductMarketing|Start with a GitLab Ultimate free trial')
- ],
- team: [
- s_('InProductMarketing|Invite your colleagues to join in less than one minute'),
- s_('InProductMarketing|Get your team set up on GitLab'),
- nil
- ]
- }[track][series]
- end
-
- def in_product_marketing_title(track, series)
- {
- create: [
- s_('InProductMarketing|Take your first steps with GitLab'),
- s_('InProductMarketing|Start by importing your projects'),
- s_('InProductMarketing|How (and why) mirroring makes sense')
- ],
- verify: [
- s_('InProductMarketing|Rapid development, simplified'),
- s_('InProductMarketing|Get started with GitLab CI/CD'),
- s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less')
- ],
- trial: [
- s_('InProductMarketing|Give us one minute...'),
- s_("InProductMarketing|Security that's integrated into your development lifecycle"),
- s_('InProductMarketing|Improve code quality and streamline reviews')
- ],
- team: [
- s_('InProductMarketing|Team work makes the dream work'),
- s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'),
- s_('InProductMarketing|Find out how your teams are really doing')
- ]
- }[track][series]
- end
-
- def in_product_marketing_subtitle(track, series)
- {
- create: [
- s_('InProductMarketing|Dig in and create a project and a repo'),
- s_("InProductMarketing|Here's what you need to know"),
- s_('InProductMarketing|Try it out')
- ],
- verify: [
- s_('InProductMarketing|How to build and test faster'),
- s_('InProductMarketing|Explore the options'),
- s_('InProductMarketing|Follow our steps')
- ],
- trial: [
- s_('InProductMarketing|...and you can get a free trial of GitLab Ultimate'),
- s_('InProductMarketing|Try GitLab Ultimate for free'),
- s_('InProductMarketing|Better code in less time')
- ],
- team: [
- s_('InProductMarketing|Actually, GitLab makes the team work (better)'),
- s_('InProductMarketing|Our tool brings all the things together'),
- s_("InProductMarketing|It's all in the stats")
- ]
- }[track][series]
- end
-
- def in_product_marketing_body_line1(track, series, format: nil)
- {
- create: [
- s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link(format), repo_link: repo_link(format) },
- s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link(format), bitbucket_link: bitbucket_link(format) },
- s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link(format) }
- ],
- verify: [
- s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link(format) },
- s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"),
- s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link(format) }
- ],
- trial: [
- [
- s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"),
- list([
- s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options(format),
- s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options(format),
- s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options(format),
- s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options(format)
- ], format)
- ].join("\n"),
- s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'),
- s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.')
- ],
- team: [
- [
- s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'),
- list([
- s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
- s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
- ], format)
- ].join("\n"),
- s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Ultimate and your teams will be on it from day one."),
- [
- s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'),
- list([
- s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
- s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
- s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
- ], format)
- ].join("\n")
- ]
- }[track][series]
- end
-
- def in_product_marketing_body_line2(track, series, format: nil)
- {
- create: [
- s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link(format) },
- s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link(format) },
- s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link(format) }
- ],
- verify: [
- nil,
- list([
- s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link(format) },
- s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link(format) },
- s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link(format) }
- ], format),
- nil
- ],
- trial: [
- s_('InProductMarketing|Start a GitLab Ultimate trial today in less than one minute, no credit card required.'),
- s_('InProductMarketing|Get started today with a 30-day GitLab Ultimate trial, no credit card required.'),
- s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Ultimate and enable these features in less than 5 minutes with no credit card required.')
- ],
- team: [
- s_('InProductMarketing|Invite your colleagues and start shipping code faster.'),
- s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."),
- s_('InProductMarketing|When your team is on GitLab these answers are a click away.')
- ]
- }[track][series]
- end
-
- def cta_link(track, series, group, format: nil)
- case format
- when :html
- link_to in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
- else
- [in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series)].join(' >> ')
- end
- end
-
- def in_product_marketing_progress(track, series, format: nil)
- if Gitlab.com?
- s_('InProductMarketing|This is email %{series} of 3 in the %{track} series.') % { series: series + 1, track: track.to_s.humanize }
- else
- s_('InProductMarketing|This is email %{series} of 3 in the %{track} series. To disable notification emails sent by your local GitLab instance, either contact your administrator or %{unsubscribe_link}.') % { series: series + 1, track: track.to_s.humanize, unsubscribe_link: unsubscribe_link(format) }
- end
- end
-
- def footer_links(format: nil)
- links = [
- [s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'],
- [s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'],
- [s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'],
- [s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg']
- ]
- case format
- when :html
- links.map do |text, link|
- link_to(text, link)
- end
- else
- '| ' + links.map do |text, link|
- [text, link].join(' ')
- end.join("\n| ")
- end
- end
-
- def address(format: nil)
- s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options(format)
- end
-
- def unsubscribe(track, series, format: nil)
- parts = Gitlab.com? ? unsubscribe_com(format) : unsubscribe_self_managed(track, series, format)
-
- case format
- when :html
- parts.join(' ')
- else
- parts.join("\n" + ' ' * 16)
- end
- end
-
- private
-
- def unsubscribe_com(format)
- [
- s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'),
- s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link(format) }
- ]
- end
-
- def unsubscribe_self_managed(track, series, format)
- [
- s_('InProductMarketing|To opt out of these onboarding emails, %{unsubscribe_link}.') % { unsubscribe_link: unsubscribe_link(format) },
- s_("InProductMarketing|If you don't want to receive marketing emails directly from GitLab, %{marketing_preference_link}.") % { marketing_preference_link: marketing_preference_link(track, series, format) }
- ]
- end
-
- def in_product_marketing_cta_text(track, series)
- {
- create: [
- s_('InProductMarketing|Create your first project!'),
- s_('InProductMarketing|Master the art of importing!'),
- s_('InProductMarketing|Understand your project options')
- ],
- verify: [
- s_('InProductMarketing|Get to know GitLab CI/CD'),
- s_('InProductMarketing|Try it yourself'),
- s_('InProductMarketing|Explore GitLab CI/CD')
- ],
- trial: [
- s_('InProductMarketing|Start a trial'),
- s_('InProductMarketing|Beef up your security'),
- s_('InProductMarketing|Start your trial now!')
- ],
- team: [
- s_('InProductMarketing|Invite your colleagues today'),
- s_('InProductMarketing|Invite your team in less than 60 seconds'),
- s_('InProductMarketing|Invite your team now')
- ]
- }[track][series]
- end
-
- def project_link(format)
- link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format)
- end
-
- def repo_link(format)
- link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'), format)
- end
-
- def github_link(format)
- link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'), format)
- end
-
- def bitbucket_link(format)
- link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'), format)
- end
-
- def mirroring_link(format)
- link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'), format)
- end
-
- def ci_link(format)
- link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'), format)
- end
-
- def performance_link(format)
- link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'), format)
- end
-
- def ci_template_link(format)
- link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'), format)
- end
-
- def deploy_link(format)
- link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'), format)
- end
-
- def quick_start_link(format)
- link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'), format)
- end
-
- def basics_link(format)
- link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'), format)
- end
-
- def import_link(format)
- link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'), format)
- end
-
- def external_repo_link(format)
- link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'), format)
- end
-
- def unsubscribe_link(format)
- unsubscribe_url = Gitlab.com? ? '%tag_unsubscribe_url%' : profile_notifications_url
-
- link(s_('InProductMarketing|unsubscribe'), unsubscribe_url, format)
- end
-
- def marketing_preference_link(track, series, format)
- params = {
- utm_source: 'SM',
- utm_medium: 'email',
- utm_campaign: 'onboarding',
- utm_term: "#{track}_#{series}"
- }
-
- preference_link = "https://about.gitlab.com/company/preference-center/?#{params.to_query}"
-
- link(s_('InProductMarketing|update your preferences'), preference_link, format)
- end
-
- def link(text, link, format)
- case format
- when :html
- link_to text, link
- else
- "#{text} (#{link})"
- end
- end
-
- def list(array, format)
- case format
- when :html
- tag.ul { array.map { |item| concat tag.li item} }
- else
- '- ' + array.join("\n- ")
- end
- end
-
- def strong_options(format)
- case format
- when :html
- { strong_start: '<b>'.html_safe, strong_end: '</b>'.html_safe }
- else
- { strong_start: '', strong_end: '' }
- end
+ def inline_image_link(image, options)
+ attachments.inline[image] = File.read(Rails.root.join("app/assets/images", image))
+ image_tag attachments[image].url, **options
end
- def inline_image_link(folder, image, options)
- attachments.inline[image] = File.read(Rails.root.join("app/assets/images", folder, image))
- image_tag attachments[image].url, **options
+ def about_link(image, width)
+ link_to inline_image_link(image, { width: width, style: "width: #{width}px;", alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/'
end
end
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 62d83ebe79e..889c058cb21 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -3,12 +3,12 @@
module InviteMembersHelper
include Gitlab::Utils::StrongMemoize
- def can_invite_members_for_group?(group)
- Feature.enabled?(:invite_members_group_modal, group) && can?(current_user, :admin_group_member, group)
+ def can_invite_members_for_project?(project)
+ Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
end
- def can_invite_members_for_project?(project)
- Feature.enabled?(:invite_members_group_modal, project.group) && can_import_members?
+ def can_invite_group_for_project?(project)
+ Feature.enabled?(:invite_members_group_modal, project.group) && project.allowed_to_share_with_group?
end
def directly_invite_members?
@@ -17,20 +17,6 @@ module InviteMembersHelper
end
end
- def indirectly_invite_members?
- strong_memoize(:indirectly_invite_members) do
- experiment_enabled?(:invite_members_version_b) && !can_import_members?
- end
- end
-
- def show_invite_members_track_event
- if directly_invite_members?
- 'show_invite_members'
- elsif indirectly_invite_members?
- 'show_invite_members_version_b'
- end
- end
-
def invite_group_members?(group)
experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
end
@@ -46,6 +32,17 @@ module InviteMembersHelper
end
end
+ def invite_accepted_notice(member)
+ case member.source
+ when Project
+ _("You have been granted %{member_human_access} access to project %{name}.") %
+ { member_human_access: member.human_access, name: member.source.name }
+ when Group
+ _("You have been granted %{member_human_access} access to group %{name}.") %
+ { member_human_access: member.human_access, name: member.source.name }
+ end
+ end
+
private
def invite_members_url(form_model)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8ebc773bb25..c662dabe453 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -199,7 +199,7 @@ module IssuablesHelper
count = issuables_count_for_state(issuable_type, state)
if count != -1
- html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill')
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm')
end
html.html_safe
@@ -332,6 +332,18 @@ module IssuablesHelper
end
end
+ def state_name_with_icon(issuable)
+ if issuable.is_a?(MergeRequest) && issuable.merged?
+ [_("Merged"), "git-merge"]
+ elsif issuable.is_a?(MergeRequest) && issuable.closed?
+ [_("Closed"), "close"]
+ elsif issuable.closed?
+ [_("Closed"), "mobile-issue-close"]
+ else
+ [_("Open"), "issue-open-m"]
+ end
+ end
+
private
def sidebar_gutter_collapsed?
@@ -386,11 +398,11 @@ module IssuablesHelper
rootPath: root_path,
fullPath: issuable[:project_full_path],
iid: issuable[:iid],
+ id: issuable[:id],
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
createNoteEmail: issuable[:create_note_email],
- issuableType: issuable[:type],
- projectMembersPath: project_project_members_path(@project, sort: :access_level_desc)
+ issuableType: issuable[:type]
}
end
@@ -414,4 +426,4 @@ module IssuablesHelper
end
end
-IssuablesHelper.prepend_if_ee('EE::IssuablesHelper')
+IssuablesHelper.prepend_mod_with('IssuablesHelper')
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 0a83e707412..1449725fb2b 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -9,6 +9,22 @@ module IssuesHelper
classes.join(' ')
end
+ def issue_manual_ordering_class
+ is_sorting_by_relative_position = @sort == 'relative_position'
+
+ if is_sorting_by_relative_position && !issue_repositioning_disabled?
+ "manual-ordering"
+ end
+ end
+
+ def issue_repositioning_disabled?
+ if @group
+ @group.root_ancestor.issue_repositioning_disabled?
+ elsif @project
+ @project.root_namespace.issue_repositioning_disabled?
+ end
+ end
+
def status_box_class(item)
if item.try(:expired?)
'status-box-expired'
@@ -165,23 +181,32 @@ module IssuesHelper
def issues_list_data(project, current_user, finder)
{
+ autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json),
+ autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
can_bulk_update: can?(current_user, :admin_issue, project).to_s,
can_edit: can?(current_user, :admin_project, project).to_s,
can_import_issues: can?(current_user, :import_issues, @project).to_s,
email: current_user&.notification_email,
+ emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: image_path('illustrations/issues.svg'),
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
- full_path: project.full_path,
has_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: import_csv_namespace_project_issues_path,
+ initial_email: project.new_issuable_address(current_user, 'issue'),
is_signed_in: current_user.present?.to_s,
issues_path: project_issues_path(project),
jira_integration_path: help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues'),
+ markdown_help_path: help_page_path('user/markdown'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }),
project_import_jira_path: project_import_jira_path(project),
+ project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
+ project_milestones_path: project_milestones_path(project, format: :json),
+ project_path: project.full_path,
+ quick_actions_help_path: help_page_path('user/project/quick_actions'),
+ reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
rss_path: url_for(safe_params.merge(rss_url_options)),
show_new_issue_link: show_new_issue_link?(project).to_s,
sign_in_path: new_user_session_path
@@ -200,4 +225,4 @@ module IssuesHelper
end
end
-IssuesHelper.prepend_if_ee('EE::IssuesHelper')
+IssuesHelper.prepend_mod_with('IssuesHelper')
diff --git a/app/helpers/kerberos_spnego_helper.rb b/app/helpers/kerberos_spnego_helper.rb
index ed09ed755fe..0f6812bc31b 100644
--- a/app/helpers/kerberos_spnego_helper.rb
+++ b/app/helpers/kerberos_spnego_helper.rb
@@ -10,4 +10,4 @@ module KerberosSpnegoHelper
end
end
-KerberosSpnegoHelper.prepend_if_ee('EE::KerberosSpnegoHelper')
+KerberosSpnegoHelper.prepend_mod_with('KerberosSpnegoHelper')
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index cfc4075100b..2150729cb2a 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -275,4 +275,4 @@ module LabelsHelper
end
end
-LabelsHelper.prepend_if_ee('EE::LabelsHelper')
+LabelsHelper.prepend_mod_with('LabelsHelper')
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
index 81896fb9fa4..a3a8a275f67 100644
--- a/app/helpers/learn_gitlab_helper.rb
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -3,11 +3,21 @@
module LearnGitlabHelper
def learn_gitlab_experiment_enabled?(project)
return false unless current_user
- return false unless experiment_enabled_for_user?
+ return false unless continous_onboarding_experiment_enabled_for_user?
learn_gitlab_onboarding_available?(project)
end
+ def learn_gitlab_experiment_tracking_category
+ return unless current_user
+
+ if Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user)
+ Gitlab::Experimentation.get_experiment(:learn_gitlab_a).tracking_category
+ elsif Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
+ Gitlab::Experimentation.get_experiment(:learn_gitlab_b).tracking_category
+ end
+ end
+
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
@@ -21,42 +31,42 @@ module LearnGitlabHelper
end
end
- private
+ def continous_onboarding_experiment_enabled_for_user?
+ Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
+ Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
+ end
- ACTION_ISSUE_IDS = {
- issue_created: 4,
- git_write: 6,
- pipeline_created: 7,
- merge_request_created: 9,
- user_added: 8,
- trial_started: 2,
- required_mr_approvals_enabled: 11,
- code_owners_enabled: 10
- }.freeze
-
- ACTION_DOC_URLS = {
- security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports'
- }.freeze
+ def onboarding_sections_data
+ {
+ workspace: {
+ svg: image_path("learn_gitlab/section_workspace.svg")
+ },
+ plan: {
+ svg: image_path("learn_gitlab/section_plan.svg")
+ },
+ deploy: {
+ svg: image_path("learn_gitlab/section_deploy.svg")
+ }
+ }
+ end
+
+ def learn_gitlab_onboarding_available?(project)
+ OnboardingProgress.onboarding?(project.namespace) &&
+ LearnGitlab::Project.new(current_user).available?
+ end
+
+ private
def action_urls
- ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS)
+ LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
+ .merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
end
def learn_gitlab_project
- @learn_gitlab_project ||= LearnGitlab.new(current_user).project
+ @learn_gitlab_project ||= LearnGitlab::Project.new(current_user).project
end
def onboarding_progress(project)
OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end
-
- def experiment_enabled_for_user?
- Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
- Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
- end
-
- def learn_gitlab_onboarding_available?(project)
- OnboardingProgress.onboarding?(project.namespace) &&
- LearnGitlab.new(current_user).available?
- end
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index ad206d0e5b5..05a55a09271 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -318,4 +318,4 @@ module MarkupHelper
extend self
end
-MarkupHelper.prepend_if_ee('EE::MarkupHelper')
+MarkupHelper.prepend_mod_with('MarkupHelper')
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 5dc636ad996..d3db5d24207 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -65,4 +65,14 @@ module MembersHelper
'group and any subresources'
end
+
+ def members_pagination_data(members, pagination = {})
+ {
+ current_page: members.respond_to?(:current_page) ? members.current_page : nil,
+ per_page: members.respond_to?(:limit_value) ? members.limit_value : nil,
+ total_items: members.respond_to?(:total_count) ? members.total_count : members.count,
+ param_name: pagination[:param_name] || nil,
+ params: pagination[:params] || {}
+ }
+ end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index df7fcb0f3da..514f5fafd65 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -29,16 +29,6 @@ module MergeRequestsHelper
classes.join(' ')
end
- def state_name_with_icon(merge_request)
- if merge_request.merged?
- [_("Merged"), "git-merge"]
- elsif merge_request.closed?
- [_("Closed"), "close"]
- else
- [_("Open"), "issue-open-m"]
- end
- end
-
def merge_path_description(merge_request, separator)
if merge_request.for_fork?
"Project:Branches: #{@merge_request.source_project_path}:#{@merge_request.source_branch} #{separator} #{@merge_request.target_project.full_path}:#{@merge_request.target_branch}"
@@ -223,4 +213,4 @@ module MergeRequestsHelper
end
end
-MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
+MergeRequestsHelper.prepend_mod_with('MergeRequestsHelper')
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 9d23ab87b98..3dfd30f07db 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -17,4 +17,4 @@ module MirrorHelper
end
end
-MirrorHelper.prepend_if_ee('EE::MirrorHelper')
+MirrorHelper.prepend_mod_with('MirrorHelper')
diff --git a/app/helpers/namespace_storage_limit_alert_helper.rb b/app/helpers/namespace_storage_limit_alert_helper.rb
index d7174c38254..ed11f89a7dd 100644
--- a/app/helpers/namespace_storage_limit_alert_helper.rb
+++ b/app/helpers/namespace_storage_limit_alert_helper.rb
@@ -6,4 +6,4 @@ module NamespaceStorageLimitAlertHelper
end
end
-NamespaceStorageLimitAlertHelper.prepend_if_ee('EE::NamespaceStorageLimitAlertHelper')
+NamespaceStorageLimitAlertHelper.prepend_mod_with('NamespaceStorageLimitAlertHelper')
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index a4521541bf9..39a8f506ba2 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -83,6 +83,15 @@ module NamespacesHelper
}
end
+ def cascading_namespace_setting_locked?(attribute, group, **args)
+ return false if group.nil?
+
+ method_name = "#{attribute}_locked?"
+ return false unless group.namespace_settings.respond_to?(method_name)
+
+ group.namespace_settings.public_send(method_name, **args) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
private
# Many importers create a temporary Group, so use the real
@@ -116,4 +125,4 @@ module NamespacesHelper
end
end
-NamespacesHelper.prepend_if_ee('EE::NamespacesHelper')
+NamespacesHelper.prepend_mod_with('NamespacesHelper')
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
new file mode 100644
index 00000000000..159b7ca87f9
--- /dev/null
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+module Nav
+ module TopNavHelper
+ PROJECTS_VIEW = :projects
+ GROUPS_VIEW = :groups
+
+ def top_nav_view_model(project:, group:)
+ builder = ::Gitlab::Nav::TopNavViewModelBuilder.new
+
+ if current_user
+ build_view_model(builder: builder, project: project, group: group)
+ else
+ build_anonymous_view_model(builder: builder)
+ end
+
+ builder.build
+ end
+
+ private
+
+ def build_anonymous_view_model(builder:)
+ # These come from `app/views/layouts/nav/_explore.html.ham`
+ if explore_nav_link?(:projects)
+ builder.add_primary_menu_item(
+ **projects_menu_item_attrs.merge(
+ {
+ active: active_nav_link?(path: %w[dashboard#show root#show projects#trending projects#starred projects#index]),
+ href: explore_root_path
+ })
+ )
+ end
+
+ if explore_nav_link?(:groups)
+ builder.add_primary_menu_item(
+ **groups_menu_item_attrs.merge(
+ {
+ active: active_nav_link?(controller: [:groups, 'groups/milestones', 'groups/group_members']),
+ href: explore_groups_path
+ })
+ )
+ end
+
+ if explore_nav_link?(:snippets)
+ builder.add_primary_menu_item(
+ **snippets_menu_item_attrs.merge(
+ {
+ active: active_nav_link?(controller: :snippets),
+ href: explore_snippets_path
+ })
+ )
+ end
+ end
+
+ def build_view_model(builder:, project:, group:)
+ # These come from `app/views/layouts/nav/_dashboard.html.haml`
+ if dashboard_nav_link?(:projects)
+ current_item = project ? current_project(project: project) : {}
+
+ builder.add_primary_menu_item(
+ **projects_menu_item_attrs.merge({
+ active: active_nav_link?(path: %w[root#index projects#trending projects#starred dashboard/projects#index]),
+ css_class: 'qa-projects-dropdown',
+ data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" },
+ view: PROJECTS_VIEW
+ })
+ )
+ builder.add_view(PROJECTS_VIEW, container_view_props(namespace: 'projects', current_item: current_item, submenu: projects_submenu))
+ end
+
+ if dashboard_nav_link?(:groups)
+ current_item = group ? current_group(group: group) : {}
+
+ builder.add_primary_menu_item(
+ **groups_menu_item_attrs.merge({
+ active: active_nav_link?(path: %w[dashboard/groups explore/groups]),
+ css_class: 'qa-groups-dropdown',
+ data: { track_label: "groups_dropdown", track_event: "click_dropdown" },
+ view: GROUPS_VIEW
+ })
+ )
+ builder.add_view(GROUPS_VIEW, container_view_props(namespace: 'groups', current_item: current_item, submenu: groups_submenu))
+ end
+
+ if dashboard_nav_link?(:milestones)
+ builder.add_primary_menu_item(
+ id: 'milestones',
+ title: 'Milestones',
+ active: active_nav_link?(controller: 'dashboard/milestones'),
+ icon: 'clock',
+ data: { qa_selector: 'milestones_link' },
+ href: dashboard_milestones_path
+ )
+ end
+
+ if dashboard_nav_link?(:snippets)
+ builder.add_primary_menu_item(
+ **snippets_menu_item_attrs.merge({
+ active: active_nav_link?(controller: 'dashboard/snippets'),
+ data: { qa_selector: 'snippets_link' },
+ href: dashboard_snippets_path
+ })
+ )
+ end
+
+ if dashboard_nav_link?(:activity)
+ builder.add_primary_menu_item(
+ id: 'activity',
+ title: 'Activity',
+ active: active_nav_link?(path: 'dashboard#activity'),
+ icon: 'history',
+ data: { qa_selector: 'activity_link' },
+ href: activity_dashboard_path
+ )
+ end
+
+ # Using admin? is generally discouraged because it does not check for
+ # "admin_mode". In this case we are migrating code and check both, so
+ # we should be good.
+ # rubocop: disable Cop/UserAdmin
+ if current_user&.admin?
+ builder.add_secondary_menu_item(
+ id: 'admin',
+ title: _('Admin'),
+ active: active_nav_link?(controller: 'admin/dashboard'),
+ icon: 'admin',
+ css_class: 'qa-admin-area-link',
+ href: admin_root_path
+ )
+ end
+
+ if Gitlab::CurrentSettings.admin_mode
+ if header_link?(:admin_mode)
+ builder.add_secondary_menu_item(
+ id: 'leave_admin_mode',
+ title: _('Leave Admin Mode'),
+ active: active_nav_link?(controller: 'admin/sessions'),
+ icon: 'lock-open',
+ href: destroy_admin_session_path,
+ method: :post
+ )
+ elsif current_user.admin?
+ builder.add_secondary_menu_item(
+ id: 'enter_admin_mode',
+ title: _('Enter Admin Mode'),
+ active: active_nav_link?(controller: 'admin/sessions'),
+ icon: 'lock',
+ href: new_admin_session_path
+ )
+ end
+ end
+ # rubocop: enable Cop/UserAdmin
+
+ if Gitlab::Sherlock.enabled?
+ builder.add_secondary_menu_item(
+ id: 'sherlock',
+ title: _('Sherlock Transactions'),
+ icon: 'admin',
+ href: sherlock_transactions_path
+ )
+ end
+ end
+
+ def projects_menu_item_attrs
+ {
+ id: 'project',
+ title: _('Projects'),
+ icon: 'project'
+ }
+ end
+
+ def groups_menu_item_attrs
+ {
+ id: 'groups',
+ title: 'Groups',
+ icon: 'group'
+ }
+ end
+
+ def snippets_menu_item_attrs
+ {
+ id: 'snippets',
+ title: _('Snippets'),
+ icon: 'snippet'
+ }
+ end
+
+ def container_view_props(namespace:, current_item:, submenu:)
+ {
+ namespace: namespace,
+ currentUserName: current_user&.username,
+ currentItem: current_item,
+ linksPrimary: submenu[:primary],
+ linksSecondary: submenu[:secondary]
+ }
+ end
+
+ def current_project(project:)
+ return {} unless project.persisted?
+
+ {
+ id: project.id,
+ name: project.name,
+ namespace: project.full_name,
+ webUrl: project_path(project),
+ avatarUrl: project.avatar_url
+ }
+ end
+
+ def current_group(group:)
+ return {} unless group.persisted?
+
+ {
+ id: group.id,
+ name: group.name,
+ namespace: group.full_name,
+ webUrl: group_path(group),
+ avatarUrl: group.avatar_url
+ }
+ end
+
+ def projects_submenu
+ # These project links come from `app/views/layouts/nav/projects_dropdown/_show.html.haml`
+ builder = ::Gitlab::Nav::TopNavMenuBuilder.new
+ builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
+ builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
+ builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
+ builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
+ builder.build
+ end
+
+ def groups_submenu
+ # These group links come from `app/views/layouts/nav/groups_dropdown/_show.html.haml`
+ builder = ::Gitlab::Nav::TopNavMenuBuilder.new
+ builder.add_primary_menu_item(id: 'your', title: _('Your groups'), href: dashboard_groups_path)
+ builder.add_primary_menu_item(id: 'explore', title: _('Explore groups'), href: explore_groups_path)
+ builder.add_secondary_menu_item(id: 'create', title: _('Create group'), href: new_group_path(anchor: 'create-group-pane'))
+ builder.build
+ end
+ end
+end
+
+Nav::TopNavHelper.prepend_mod
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index db144f63f92..aab1a44bdfb 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -12,6 +12,7 @@ module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
+ class_name << 'sidebar-refactoring' if Feature.enabled?(:sidebar_refactor, current_user)
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
@@ -68,7 +69,14 @@ module NavHelper
end
def group_issues_sub_menu_items
- %w(groups#issues labels#index milestones#index boards#index boards#show)
+ %w[
+ groups#issues
+ milestones#index
+ boards#index
+ boards#show
+ ].tap do |paths|
+ paths << 'labels#index' if Feature.disabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
+ end
end
private
@@ -100,4 +108,4 @@ module NavHelper
end
end
-NavHelper.prepend_if_ee('EE::NavHelper')
+NavHelper.prepend_mod_with('NavHelper')
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 62580124c0f..fff7e5d1c7f 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -215,4 +215,4 @@ module NotesHelper
end
end
-NotesHelper.prepend_if_ee('EE::NotesHelper')
+NotesHelper.prepend_mod_with('NotesHelper')
diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb
index 03da679cfdd..38c98776fdf 100644
--- a/app/helpers/notify_helper.rb
+++ b/app/helpers/notify_helper.rb
@@ -18,7 +18,7 @@ module NotifyHelper
when "Developer"
s_("InviteEmail|As a developer, you have full access to projects, so you can take an idea from concept to production.")
when "Maintainer"
- s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to master and deploy to production.")
+ s_("InviteEmail|As a maintainer, you have full access to projects. You can push commits to the default branch and deploy to production.")
when "Owner"
s_("InviteEmail|As an owner, you have full access to projects and can manage access to the group, including inviting new members.")
when "Minimal Access"
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 51f4304911b..df07baa2d03 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -44,4 +44,4 @@ module OperationsHelper
end
end
-OperationsHelper.prepend_if_ee('EE::OperationsHelper')
+OperationsHelper.prepend_mod_with('OperationsHelper')
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 6997c8cffda..2729951d685 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -162,7 +162,6 @@ module PageLayoutHelper
default_properties = {
current_emoji: '',
current_message: '',
- can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml),
default_emoji: UserStatus::DEFAULT_EMOJI
}
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index add6e1eaf6f..d851ed3db8f 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -4,8 +4,8 @@
module PreferencesHelper
def layout_choices
[
- ['Fixed', :fixed],
- ['Fluid', :fluid]
+ ['Fixed', :fixed],
+ ['Fluid', :fluid]
]
end
@@ -76,7 +76,7 @@ module PreferencesHelper
def language_choices
options_for_select(
- Gitlab::I18n.selectable_locales.map(&:reverse).sort,
+ selectable_locales_with_translation_level.sort,
current_user.preferred_language
)
end
@@ -107,6 +107,18 @@ module PreferencesHelper
def default_first_day_of_week
first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first
end
+
+ def selectable_locales_with_translation_level
+ Gitlab::I18n.selectable_locales.map do |code, language|
+ [
+ s_("i18n|%{language} (%{percent_translated}%% translated)") % {
+ language: language,
+ percent_translated: Gitlab::I18n.percentage_translated_for(code)
+ },
+ code
+ ]
+ end
+ end
end
-PreferencesHelper.prepend_if_ee('EE::PreferencesHelper')
+PreferencesHelper.prepend_mod_with('PreferencesHelper')
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 3219620de71..f6ed567c9ea 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -51,4 +51,4 @@ module ProfilesHelper
end
end
-ProfilesHelper.prepend_ee_mod
+ProfilesHelper.prepend_mod
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index b705258f133..b46e3eb3bc3 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -10,6 +10,7 @@ module Projects::AlertManagementHelper
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s,
+ 'has-managed-prometheus' => has_managed_prometheus?(project).to_s,
'text-query': params[:search],
'assignee-username-query': params[:assignee_username]
}
@@ -27,6 +28,10 @@ module Projects::AlertManagementHelper
private
+ def has_managed_prometheus?(project)
+ project.prometheus_service&.prometheus_available? == true
+ end
+
def alert_management_enabled?(project)
!!(
project.alert_management_alerts.any? ||
diff --git a/app/helpers/projects/incidents_helper.rb b/app/helpers/projects/incidents_helper.rb
index 63504cb55b9..dde2980817f 100644
--- a/app/helpers/projects/incidents_helper.rb
+++ b/app/helpers/projects/incidents_helper.rb
@@ -16,4 +16,4 @@ module Projects::IncidentsHelper
end
end
-Projects::IncidentsHelper.prepend_if_ee('EE::Projects::IncidentsHelper')
+Projects::IncidentsHelper.prepend_mod_with('Projects::IncidentsHelper')
diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb
index 662afbcfd25..fa68bbad135 100644
--- a/app/helpers/projects/project_members_helper.rb
+++ b/app/helpers/projects/project_members_helper.rb
@@ -27,29 +27,41 @@ module Projects::ProjectMembersHelper
project.group.has_owner?(current_user)
end
- def project_group_links_data_json(group_links)
- GroupLink::ProjectGroupLinkSerializer.new.represent(group_links, { current_user: current_user }).to_json
+ def project_members_list_data_json(project, members, pagination = {})
+ project_members_list_data(project, members, pagination).to_json
end
- def project_members_data_json(project, members)
- MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project }).to_json
+ def project_group_links_list_data_json(project, group_links)
+ project_group_links_list_data(project, group_links).to_json
end
- def project_members_list_data_attributes(project, members)
+ private
+
+ def project_members_serialized(project, members)
+ MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project })
+ end
+
+ def project_group_links_serialized(group_links)
+ GroupLink::ProjectGroupLinkSerializer.new.represent(group_links, { current_user: current_user })
+ end
+
+ def project_members_list_data(project, members, pagination)
{
- members: project_members_data_json(project, members),
+ members: project_members_serialized(project, members),
+ pagination: members_pagination_data(members, pagination),
member_path: project_project_member_path(project, ':id'),
source_id: project.id,
- can_manage_members: can_manage_project_members?(project).to_s
+ can_manage_members: can_manage_project_members?(project)
}
end
- def project_group_links_list_data_attributes(project, group_links)
+ def project_group_links_list_data(project, group_links)
{
- members: project_group_links_data_json(group_links),
+ members: project_group_links_serialized(group_links),
+ pagination: members_pagination_data(group_links),
member_path: project_group_link_path(project, ':id'),
source_id: project.id,
- can_manage_members: can_manage_project_members?(project).to_s
+ can_manage_members: can_manage_project_members?(project)
}
end
end
diff --git a/app/helpers/projects/security/configuration_helper.rb b/app/helpers/projects/security/configuration_helper.rb
index 265d46cbc41..dee106ab3ae 100644
--- a/app/helpers/projects/security/configuration_helper.rb
+++ b/app/helpers/projects/security/configuration_helper.rb
@@ -10,4 +10,4 @@ module Projects
end
end
-::Projects::Security::ConfigurationHelper.prepend_if_ee('::EE::Projects::Security::ConfigurationHelper')
+::Projects::Security::ConfigurationHelper.prepend_mod_with('Projects::Security::ConfigurationHelper')
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 4be6cd4276b..f2a50ce1325 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -125,34 +125,12 @@ module ProjectsHelper
project.fork_source if project.fork_source && can?(current_user, :read_project, project.fork_source)
end
- def project_nav_tabs
- @nav_tabs ||= get_project_nav_tabs(@project, current_user)
- end
-
def project_search_tabs?(tab)
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
end
- def project_nav_tab?(name)
- project_nav_tabs.include? name
- end
-
- def any_project_nav_tab?(tabs)
- tabs.any? { |tab| project_nav_tab?(tab) }
- end
-
- def project_for_deploy_key(deploy_key)
- if deploy_key.has_access_to?(@project)
- @project
- else
- deploy_key.projects.find do |project|
- can?(current_user, :read_project, project)
- end
- end
- end
-
def can_change_visibility_level?(project, current_user)
can?(current_user, :change_visibility_level, project)
end
@@ -285,10 +263,6 @@ module ProjectsHelper
!disabled && !compact_mode
end
- def settings_operations_available?
- !@project.archived? && can?(current_user, :admin_operations, @project)
- end
-
def error_tracking_setting_project_json
setting = @project.error_tracking_setting
@@ -378,89 +352,6 @@ module ProjectsHelper
private
- def can_read_security_configuration?(project, current_user)
- can?(current_user, :access_security_and_compliance, project) &&
- can?(current_user, :read_security_configuration, project)
- end
-
- def get_project_security_nav_tabs(project, current_user)
- if can_read_security_configuration?(project, current_user)
- [:security_and_compliance, :security_configuration]
- else
- []
- end
- end
-
- # rubocop:disable Metrics/CyclomaticComplexity
- def get_project_nav_tabs(project, current_user)
- nav_tabs = [:home]
-
- unless project.empty_repo?
- nav_tabs += [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
- nav_tabs << :releases if can?(current_user, :read_release, project)
- end
-
- nav_tabs += get_project_security_nav_tabs(project, current_user)
-
- if project.repo_exists? && can?(current_user, :read_merge_request, project)
- nav_tabs << :merge_requests
- end
-
- if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
- nav_tabs << :container_registry
- end
-
- if Feature.enabled?(:infrastructure_registry_page)
- nav_tabs << :infrastructure_registry
- end
-
- # Pipelines feature is tied to presence of builds
- if can?(current_user, :read_build, project)
- nav_tabs << :pipelines
- end
-
- if can_view_operations_tab?(current_user, project)
- nav_tabs << :operations
- end
-
- if can_view_product_analytics?(current_user, project)
- nav_tabs << :product_analytics
- end
-
- tab_ability_map.each do |tab, ability|
- if can?(current_user, ability, project)
- nav_tabs << tab
- end
- end
-
- apply_external_nav_tabs(nav_tabs, project)
-
- nav_tabs += package_nav_tabs(project, current_user)
-
- nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project)
-
- nav_tabs
- end
- # rubocop:enable Metrics/CyclomaticComplexity
-
- def package_nav_tabs(project, current_user)
- [].tap do |tabs|
- if ::Gitlab.config.packages.enabled && can?(current_user, :read_package, project)
- tabs << :packages
- end
- end
- end
-
- def apply_external_nav_tabs(nav_tabs, project)
- nav_tabs << :external_issue_tracker if project.external_issue_tracker
- nav_tabs << :external_wiki if project.external_wiki
-
- if project.has_confluence?
- nav_tabs.delete(:wiki)
- nav_tabs << :confluence
- end
- end
-
def tab_ability_map
{
cycle_analytics: :read_cycle_analytics,
@@ -485,32 +376,6 @@ module ProjectsHelper
}
end
- def view_operations_tab_ability
- [
- :metrics_dashboard,
- :read_alert_management_alert,
- :read_environment,
- :read_issue,
- :read_sentry_issue,
- :read_cluster,
- :read_feature_flag,
- :read_terraform_state
- ]
- end
-
- def can_view_operations_tab?(current_user, project)
- return false unless project.feature_available?(:operations, current_user)
-
- view_operations_tab_ability.any? do |ability|
- can?(current_user, ability, project)
- end
- end
-
- def can_view_product_analytics?(current_user, project)
- Feature.enabled?(:product_analytics, project) &&
- can?(current_user, :read_product_analytics, project)
- end
-
def search_tab_ability_map
@search_tab_ability_map ||= tab_ability_map.merge(
blobs: :download_code,
@@ -578,14 +443,6 @@ module ProjectsHelper
end
end
- def sidebar_operations_link_path(project = @project)
- if can?(current_user, :read_environment, project)
- metrics_project_environments_path(project)
- else
- project_feature_flags_path(project)
- end
- end
-
def project_last_activity(project)
if project.last_activity_at
time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
@@ -723,29 +580,6 @@ module ProjectsHelper
"#{request.path}?#{options.to_param}"
end
- def sidebar_security_configuration_paths
- %w[
- projects/security/configuration#show
- ]
- end
-
- def sidebar_settings_paths
- %w[
- projects#edit
- integrations#show
- services#edit
- hooks#index
- hooks#edit
- access_tokens#index
- hook_logs#show
- repository#show
- ci_cd#show
- operations#show
- badges#index
- pages#show
- ]
- end
-
def sidebar_operations_paths
%w[
environments
@@ -766,10 +600,6 @@ module ProjectsHelper
]
end
- def sidebar_security_paths
- %w[projects/security/configuration#show]
- end
-
def user_can_see_auto_devops_implicitly_enabled_banner?(project, user)
Ability.allowed?(user, :admin_project, project) &&
project.has_auto_devops_implicitly_enabled? &&
@@ -782,6 +612,16 @@ module ProjectsHelper
end
def settings_container_registry_expiration_policy_available?(project)
+ Feature.disabled?(:sidebar_refactor, current_user) &&
+ can_destroy_container_registry_image?(current_user, project)
+ end
+
+ def settings_packages_and_registries_enabled?(project)
+ Feature.enabled?(:sidebar_refactor, current_user) &&
+ can_destroy_container_registry_image?(current_user, project)
+ end
+
+ def can_destroy_container_registry_image?(current_user, project)
Gitlab.config.registry.enabled &&
can?(current_user, :destroy_container_image, project)
end
@@ -811,4 +651,4 @@ module ProjectsHelper
end
end
-ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
+ProjectsHelper.prepend_mod_with('ProjectsHelper')
diff --git a/app/helpers/registrations_helper.rb b/app/helpers/registrations_helper.rb
new file mode 100644
index 00000000000..79f0a66f995
--- /dev/null
+++ b/app/helpers/registrations_helper.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module RegistrationsHelper
+ def social_signin_enabled?
+ ::Gitlab.dev_env_or_com? &&
+ omniauth_enabled? &&
+ devise_mapping.omniauthable? &&
+ button_based_providers_enabled?
+ end
+end
+
+RegistrationsHelper.prepend_mod_with('RegistrationsHelper')
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index d9851564585..de9288121c4 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -72,4 +72,4 @@ module ReleasesHelper
end
end
-ReleasesHelper.prepend_if_ee('EE::ReleasesHelper')
+ReleasesHelper.prepend_mod_with('ReleasesHelper')
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 2568917bafc..1f4c98d6f28 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -310,7 +310,7 @@ module SearchHelper
link_to search_path(search_params) do
concat label
concat ' '
- concat content_tag(:span, count, class: ['badge badge-pill', badge_class], data: badge_data)
+ concat content_tag(:span, count, class: ['badge badge-pill gl-badge badge-muted sm', badge_class], data: badge_data)
end
end
end
@@ -431,4 +431,4 @@ module SearchHelper
end
end
-SearchHelper.prepend_if_ee('EE::SearchHelper')
+SearchHelper.prepend_mod_with('SearchHelper')
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 4d0f9e530fb..88aff31af54 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -91,4 +91,4 @@ module SelectsHelper
end
end
-SelectsHelper.prepend_if_ee('EE::SelectsHelper')
+SelectsHelper.prepend_mod_with('SelectsHelper')
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index ffa09cb12fb..3d3ab3a6972 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -153,9 +153,9 @@ module ServicesHelper
private
def integration_level(integration)
- if integration.instance
+ if integration.instance_level?
'instance'
- elsif integration.group_id
+ elsif integration.group_level?
'group'
else
'project'
@@ -172,10 +172,14 @@ module ServicesHelper
name: integration.to_param
}
end
+
+ def show_service_templates_nav_link?
+ Feature.disabled?(:disable_service_templates, type: :development, default_enabled: :yaml)
+ end
end
-ServicesHelper.prepend_if_ee('EE::ServicesHelper')
+ServicesHelper.prepend_mod_with('ServicesHelper')
# The methods in `EE::ServicesHelper` should be available as both instance and
# class methods.
-ServicesHelper.extend_if_ee('EE::ServicesHelper')
+ServicesHelper.extend_mod_with('ServicesHelper')
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 31dfe21671a..0fc306a3f2e 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -39,7 +39,13 @@ module SidebarsHelper
current_user: user,
container: project,
learn_gitlab_experiment_enabled: learn_gitlab_experiment_enabled?(project),
- current_ref: current_ref
+ learn_gitlab_experiment_tracking_category: learn_gitlab_experiment_tracking_category,
+ current_ref: current_ref,
+ jira_issues_integration: project_jira_issues_integration?,
+ can_view_pipeline_editor: can_view_pipeline_editor?(project),
+ show_cluster_hint: show_gke_cluster_integration_callout?(project)
}
end
end
+
+SidebarsHelper.prepend_mod_with('SidebarsHelper')
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index f4af7a5a350..84eb0405c01 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -72,4 +72,10 @@ module SnippetsHelper
concat(file_count)
end
end
+
+ def project_snippets_award_api_path(snippet)
+ if Feature.enabled?(:improved_emoji_picker, snippet.project, default_enabled: :yaml)
+ api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
+ end
+ end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 974ec046bbb..0bb9e9e9bdd 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -301,4 +301,4 @@ module SortingHelper
end
end
-SortingHelper.prepend_if_ee('::EE::SortingHelper')
+SortingHelper.prepend_mod_with('SortingHelper')
diff --git a/app/helpers/sorting_titles_values_helper.rb b/app/helpers/sorting_titles_values_helper.rb
index 651a6437479..28d70f1db45 100644
--- a/app/helpers/sorting_titles_values_helper.rb
+++ b/app/helpers/sorting_titles_values_helper.rb
@@ -328,4 +328,4 @@ module SortingTitlesValuesHelper
end
end
-SortingHelper.include_if_ee('::EE::SortingTitlesValuesHelper')
+SortingHelper.include_mod_with('SortingTitlesValuesHelper')
diff --git a/app/helpers/ssh_keys_helper.rb b/app/helpers/ssh_keys_helper.rb
index 381db893943..f5a9bea482b 100644
--- a/app/helpers/ssh_keys_helper.rb
+++ b/app/helpers/ssh_keys_helper.rb
@@ -12,7 +12,10 @@ module SshKeysHelper
message: _('This action cannot be undone, and will permanently delete the %{key} SSH key') % { key: key.title },
okVariant: 'danger',
okTitle: _('Delete')
- }
+ },
+ toggle: 'tooltip',
+ placement: 'top',
+ container: 'body'
}
end
end
diff --git a/app/helpers/subscribable_banner_helper.rb b/app/helpers/subscribable_banner_helper.rb
index c9d4370f8ad..d9251fb3f21 100644
--- a/app/helpers/subscribable_banner_helper.rb
+++ b/app/helpers/subscribable_banner_helper.rb
@@ -6,4 +6,4 @@ module SubscribableBannerHelper
end
end
-SubscribableBannerHelper.prepend_if_ee('EE::SubscribableBannerHelper')
+SubscribableBannerHelper.prepend_mod_with('SubscribableBannerHelper')
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 85e644967ea..521423fbb94 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -54,8 +54,8 @@ module SystemNoteHelper
extend self
end
-SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper')
+SystemNoteHelper.prepend_mod_with('SystemNoteHelper')
# The methods in `EE::SystemNoteHelper` should be available as both instance and
# class methods.
-SystemNoteHelper.extend_if_ee('EE::SystemNoteHelper')
+SystemNoteHelper.extend_mod_with('SystemNoteHelper')
diff --git a/app/helpers/time_zone_helper.rb b/app/helpers/time_zone_helper.rb
index 00f65b72c8e..fe045182c96 100644
--- a/app/helpers/time_zone_helper.rb
+++ b/app/helpers/time_zone_helper.rb
@@ -18,7 +18,7 @@ module TimeZoneHelper
def timezone_data(format: :short)
attrs = TIME_ZONE_FORMAT_ATTRS.fetch(format) do
valid_formats = TIME_ZONE_FORMAT_ATTRS.keys.map { |k| ":#{k}"}.join(", ")
- raise ArgumentError.new("Invalid format :#{format}. Valid formats are #{valid_formats}.")
+ raise ArgumentError, "Invalid format :#{format}. Valid formats are #{valid_formats}."
end
ActiveSupport::TimeZone.all.map do |timezone|
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index e034a985b50..0993e210f42 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -288,4 +288,4 @@ module TimeboxesHelper
end
end
-TimeboxesHelper.prepend_if_ee('EE::TimeboxesHelper')
+TimeboxesHelper.prepend_mod_with('TimeboxesHelper')
diff --git a/app/helpers/timeboxes_routing_helper.rb b/app/helpers/timeboxes_routing_helper.rb
index 6fb5a1a3185..6a5bef74dc9 100644
--- a/app/helpers/timeboxes_routing_helper.rb
+++ b/app/helpers/timeboxes_routing_helper.rb
@@ -18,4 +18,4 @@ module TimeboxesRoutingHelper
end
end
-TimeboxesRoutingHelper.prepend_if_ee('EE::TimeboxesRoutingHelper')
+TimeboxesRoutingHelper.prepend_mod_with('TimeboxesRoutingHelper')
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index e9a0fef06c4..e9dc271dbdd 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -110,10 +110,8 @@ module TodosHelper
'alert'
end
- content_tag(:span, nil, class: 'target-status') do
- content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}") do
- todo.target.state.to_s.capitalize
- end
+ tag.span class: "gl-my-0 gl-px-2 status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}" do
+ todo.target.state.to_s.capitalize
end
end
@@ -232,4 +230,4 @@ module TodosHelper
end
end
-TodosHelper.prepend_if_ee('EE::TodosHelper')
+TodosHelper.prepend_mod_with('TodosHelper')
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b795851ba30..54c03d3d966 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -205,4 +205,4 @@ module TreeHelper
end
end
-TreeHelper.prepend_if_ee('::EE::TreeHelper')
+TreeHelper.prepend_mod_with('TreeHelper')
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 7a90984cd77..23db3be631c 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -44,7 +44,7 @@ module UserCalloutsHelper
def show_service_templates_deprecated_callout?
!Gitlab.com? &&
current_user&.admin? &&
- Service.for_template.active.exists? &&
+ Integration.for_template.active.exists? &&
!user_dismissed?(SERVICE_TEMPLATES_DEPRECATED_CALLOUT)
end
@@ -80,4 +80,4 @@ module UserCalloutsHelper
end
end
-UserCalloutsHelper.prepend_if_ee('EE::UserCalloutsHelper')
+UserCalloutsHelper.prepend_mod_with('UserCalloutsHelper')
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1979426f844..c1d05c2d3cf 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -100,7 +100,7 @@ module UsersHelper
badges << blocked_user_badge(user) if user.blocked?
badges << { text: s_('AdminUsers|Admin'), variant: 'success' } if user.admin?
badges << { text: s_('AdminUsers|External'), variant: 'secondary' } if user.external?
- badges << { text: s_("AdminUsers|It's you!"), variant: nil } if current_user == user
+ badges << { text: s_("AdminUsers|It's you!"), variant: 'muted' } if current_user == user
end
end
@@ -162,6 +162,49 @@ module UsersHelper
header + list
end
+ def user_ban_data(user)
+ {
+ path: ban_admin_user_path(user),
+ method: 'put',
+ modal_attributes: {
+ title: s_('AdminUsers|Ban user %{username}?') % { username: sanitize_name(user.name) },
+ message: s_('AdminUsers|You can unban their account in the future. Their data remains intact.'),
+ okVariant: 'warning',
+ okTitle: s_('AdminUsers|Ban')
+ }.to_json
+ }
+ end
+
+ def user_unban_data(user)
+ {
+ path: unban_admin_user_path(user),
+ method: 'put',
+ modal_attributes: {
+ title: s_('AdminUsers|Unban %{username}?') % { username: sanitize_name(user.name) },
+ message: s_('AdminUsers|You ban their account in the future if necessary.'),
+ okVariant: 'info',
+ okTitle: s_('AdminUsers|Unban')
+ }.to_json
+ }
+ end
+
+ def user_ban_effects
+ header = tag.p s_('AdminUsers|Banning the user has the following effects:')
+
+ list = tag.ul do
+ concat tag.li s_('AdminUsers|User will be blocked')
+ end
+
+ link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") }
+ info = tag.p s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+
+ header + list + info
+ end
+
+ def ban_feature_available?
+ Feature.enabled?(:ban_user_feature_flag)
+ end
+
def user_deactivation_data(user, message)
{
path: deactivate_admin_user_path(user),
@@ -235,6 +278,9 @@ module UsersHelper
pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' }
return pending_approval_badge if user.blocked_pending_approval?
+ banned_badge = { text: s_('AdminUsers|Banned'), variant: 'danger' }
+ return banned_badge if user.banned?
+
{ text: s_('AdminUsers|Blocked'), variant: 'danger' }
end
@@ -322,4 +368,4 @@ module UsersHelper
end
end
-UsersHelper.prepend_if_ee('EE::UsersHelper')
+UsersHelper.prepend_mod_with('UsersHelper')
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index bac3c99e3e5..6f94c241914 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -11,16 +11,24 @@ module VersionCheckHelper
def link_to_version
if Gitlab.pre_release?
- commit_link = link_to(Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', source_code_project, Gitlab.revision))
+ commit_link = link_to(Gitlab.revision, source_host_url + namespace_project_commits_path(source_code_group, source_code_project, Gitlab.revision))
[Gitlab::VERSION, content_tag(:small, commit_link)].join(' ').html_safe
else
- link_to Gitlab::VERSION, Gitlab::COM_URL + namespace_project_tag_path('gitlab-org', source_code_project, "v#{Gitlab::VERSION}")
+ link_to Gitlab::VERSION, source_host_url + namespace_project_tag_path(source_code_group, source_code_project, "v#{Gitlab::VERSION}")
end
end
+ def source_host_url
+ Gitlab::COM_URL
+ end
+
+ def source_code_group
+ 'gitlab-org'
+ end
+
def source_code_project
'gitlab-foss'
end
end
-VersionCheckHelper.prepend_if_ee('EE::VersionCheckHelper')
+VersionCheckHelper.prepend_mod
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 170e3c45a21..0d27e07f172 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,10 +1,32 @@
# frozen_string_literal: true
module WebpackHelper
+ def prefetch_link_tag(source)
+ href = asset_path(source)
+
+ link_tag = tag.link(rel: 'prefetch', href: href)
+
+ early_hints_link = "<#{href}>; rel=prefetch"
+
+ request.send_early_hints("Link" => early_hints_link)
+
+ link_tag
+ end
+
def webpack_bundle_tag(bundle)
javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
+ def webpack_preload_asset_tag(asset, options = {})
+ path = Gitlab::Webpack::Manifest.asset_paths(asset).first
+
+ if options.delete(:prefetch)
+ prefetch_link_tag(path)
+ else
+ preload_link_tag(path, options)
+ end
+ end
+
def webpack_controller_bundle_tags
chunks = []
diff --git a/app/helpers/whats_new_helper.rb b/app/helpers/whats_new_helper.rb
index 9362ae1491f..5fca00c5dce 100644
--- a/app/helpers/whats_new_helper.rb
+++ b/app/helpers/whats_new_helper.rb
@@ -10,6 +10,33 @@ module WhatsNewHelper
end
def display_whats_new?
- Gitlab.dev_env_org_or_com? || user_signed_in?
+ (Gitlab.dev_env_org_or_com? || user_signed_in?) &&
+ !Gitlab::CurrentSettings.current_application_settings.whats_new_variant_disabled?
+ end
+
+ def whats_new_variants
+ ApplicationSetting.whats_new_variants
+ end
+
+ def whats_new_variants_label(variant)
+ case variant
+ when 'all_tiers'
+ _("Enable What's new: All tiers")
+ when 'current_tier'
+ _("Enable What's new: Current tier only")
+ when 'disabled'
+ _("Disable What's new")
+ end
+ end
+
+ def whats_new_variants_description(variant)
+ case variant
+ when 'all_tiers'
+ _("What's new presents new features from all tiers to help you keep track of all new features.")
+ when 'current_tier'
+ _("What's new presents new features for your current subscription tier, while hiding new features not available to your subscription tier.")
+ when 'disabled'
+ _("What's new is disabled and can no longer be viewed.")
+ end
end
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index c2a5ff40852..1b0d1254dc8 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -136,4 +136,4 @@ module WikiHelper
end
end
-WikiHelper.prepend_if_ee('EE::WikiHelper')
+WikiHelper.prepend_mod_with('WikiHelper')
diff --git a/app/helpers/x509_helper.rb b/app/helpers/x509_helper.rb
index 009635fb629..4afc5643af4 100644
--- a/app/helpers/x509_helper.rb
+++ b/app/helpers/x509_helper.rb
@@ -13,7 +13,7 @@ module X509Helper
end
subjects
- rescue
+ rescue StandardError
{}
end
diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb
index d21c3d13b10..97243660512 100644
--- a/app/mailers/emails/in_product_marketing.rb
+++ b/app/mailers/emails/in_product_marketing.rb
@@ -2,8 +2,6 @@
module Emails
module InProductMarketing
- include InProductMarketingHelper
-
FROM_ADDRESS = 'GitLab <team@gitlab.com>'
CUSTOM_HEADERS = {
from: FROM_ADDRESS,
@@ -15,13 +13,11 @@ module Emails
}.freeze
def in_product_marketing_email(recipient_id, group_id, track, series)
- @track = track
- @series = series
- @group = Group.find(group_id)
+ group = Group.find(group_id)
+ email = User.find(recipient_id).notification_email_for(group)
+ @message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series)
- email = User.find(recipient_id).notification_email_for(@group)
- subject = subject_line(track, series)
- mail_to(to: email, subject: subject)
+ mail_to(to: email, subject: @message.subject_line)
end
private
@@ -29,8 +25,17 @@ module Emails
def mail_to(to:, subject:)
custom_headers = Gitlab.com? ? CUSTOM_HEADERS : {}
mail(to: to, subject: subject, **custom_headers) do |format|
- format.html { render layout: nil }
- format.text { render layout: nil }
+ format.html do
+ @message.format = :html
+
+ render layout: nil
+ end
+
+ format.text do
+ @message.format = :text
+
+ render layout: nil
+ end
end
end
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index c565df1a2ee..51c4779d8cf 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -139,4 +139,4 @@ module Emails
end
end
-Emails::Issues.prepend_if_ee('EE::Emails::Issues')
+Emails::Issues.prepend_mod_with('Emails::Issues')
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index f4d3676dc5c..674a9bfc4eb 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -167,4 +167,4 @@ module Emails
end
end
-Emails::Members.prepend_if_ee('EE::Emails::Members')
+Emails::Members.prepend_mod_with('Emails::Members')
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index e538b5e4718..2746b8b7188 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -167,4 +167,4 @@ module Emails
end
end
-Emails::MergeRequests.prepend_if_ee('EE::Emails::MergeRequests')
+Emails::MergeRequests.prepend_mod_with('Emails::MergeRequests')
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 4b56ff60f09..587c1479286 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -84,4 +84,4 @@ module Emails
end
end
-Emails::Notes.prepend_if_ee('EE::Emails::Notes')
+Emails::Notes.prepend_mod_with('Emails::Notes')
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index f967323f849..2efcba54c13 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -132,4 +132,4 @@ module Emails
end
end
-Emails::Profile.prepend_if_ee('EE::Emails::Profile')
+Emails::Profile.prepend_mod_with('Emails::Profile')
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index a4b7b140169..2ae82b49609 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -66,4 +66,4 @@ module Emails
end
end
-Emails::Projects.prepend_if_ee('EE::Emails::Projects')
+Emails::Projects.prepend_mod_with('Emails::Projects')
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5f5afef350b..dd75ab4bf03 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -223,4 +223,4 @@ class Notify < ApplicationMailer
end
end
-Notify.prepend_if_ee('EE::Notify')
+Notify.prepend_mod_with('Notify')
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 5fda60a7408..df0d1774d6b 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -88,6 +88,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.issues_csv_email(user, project, '1997,Ford,E350', { truncated: false, rows_expected: 3, rows_written: 3 }).message
end
+ def new_issue_email
+ Notify.new_issue_email(user.id, issue.id).message
+ end
+
def new_merge_request_email
Notify.new_merge_request_email(user.id, merge_request.id).message
end
@@ -200,7 +204,7 @@ class NotifyPreview < ActionMailer::Preview
end
def issue
- @merge_request ||= project.issues.first
+ @issue ||= project.issues.first
end
def merge_request
@@ -251,4 +255,4 @@ class NotifyPreview < ActionMailer::Preview
end
end
-NotifyPreview.prepend_if_ee('EE::Preview::NotifyPreview')
+NotifyPreview.prepend_mod_with('Preview::NotifyPreview')
diff --git a/app/models/ability.rb b/app/models/ability.rb
index ba46a98b951..c18bd21d754 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
class Ability
class << self
# Given a list of users and a project this method returns the users that can
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 7090d9f4ea1..156111ffaf3 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -20,7 +20,13 @@ module AlertManagement
resolved: 2,
ignored: 3
}.freeze
- private_constant :STATUSES
+
+ STATUS_DESCRIPTIONS = {
+ triggered: 'Investigation has not started',
+ acknowledged: 'Someone is actively investigating the problem',
+ resolved: 'No further work is required',
+ ignored: 'No action will be taken on the alert'
+ }.freeze
belongs_to :project
belongs_to :issue, optional: true
@@ -271,4 +277,4 @@ module AlertManagement
end
end
-AlertManagement::Alert.prepend_if_ee('EE::AlertManagement::Alert')
+AlertManagement::Alert.prepend_mod_with('AlertManagement::Alert')
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index e98c770c364..2caa9a18445 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -10,7 +10,7 @@ module AlertManagement
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
default_value_for(:endpoint_identifier, allows_nil: false) { SecureRandom.hex(8) }
diff --git a/app/models/alerting/project_alerting_setting.rb b/app/models/alerting/project_alerting_setting.rb
index 8f8c38f11e4..34fa27eb29b 100644
--- a/app/models/alerting/project_alerting_setting.rb
+++ b/app/models/alerting/project_alerting_setting.rb
@@ -10,7 +10,7 @@ module Alerting
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
before_validation :ensure_token
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index b2c16444a2a..e8b03fa066a 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -7,10 +7,13 @@ module Analytics
validates :project, presence: true
belongs_to :project
+ belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
+ alias_attribute :value_stream_id, :project_value_stream_id
+
delegate :group, to: :project
validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
diff --git a/app/models/analytics/cycle_analytics/project_value_stream.rb b/app/models/analytics/cycle_analytics/project_value_stream.rb
new file mode 100644
index 00000000000..3eba7e87b17
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/project_value_stream.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class Analytics::CycleAnalytics::ProjectValueStream < ApplicationRecord
+ belongs_to :project
+
+ has_many :stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+
+ validates :project, :name, presence: true
+ validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :project_id }
+
+ def custom?
+ false
+ end
+
+ def stages
+ []
+ end
+
+ def self.build_default_value_stream(project)
+ new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, project: project)
+ end
+end
diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb
index ad0272699c2..46c5d56d210 100644
--- a/app/models/analytics/usage_trends/measurement.rb
+++ b/app/models/analytics/usage_trends/measurement.rb
@@ -58,4 +58,4 @@ module Analytics
end
end
-Analytics::UsageTrends::Measurement.prepend_if_ee('EE::Analytics::UsageTrends::Measurement')
+Analytics::UsageTrends::Measurement.prepend_mod_with('Analytics::UsageTrends::Measurement')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 1bbace791ed..5e5bc00458e 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -66,6 +66,12 @@ class ApplicationRecord < ActiveRecord::Base
end
end
+ def create_or_load_association(association_name)
+ association(association_name).create unless association(association_name).loaded?
+ rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
+ association(association_name).reader
+ end
+
def self.underscore
Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
end
@@ -80,4 +86,4 @@ class ApplicationRecord < ActiveRecord::Base
end
end
-ApplicationRecord.prepend_if_ee('EE::ApplicationRecordHelpers')
+ApplicationRecord.prepend_mod_with('ApplicationRecordHelpers')
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f405f5ca5d3..65800e40d6c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,8 @@ class ApplicationSetting < ApplicationRecord
KROKI_URL_ERROR_MESSAGE = 'Please check your Kroki URL setting in ' \
'Admin Area > Settings > General > Kroki'
+ enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
+
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
@@ -132,6 +134,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :akismet_enabled
+ validates :spam_check_api_key,
+ length: { maximum: 2000, message: _('is too long (maximum is %{count} characters)') },
+ allow_blank: true
+
+ validates :spam_check_api_key,
+ presence: true,
+ if: :spam_check_endpoint_enabled
+
validates :unique_ips_limit_per_user,
numericality: { greater_than_or_equal_to: 1 },
presence: true,
@@ -365,7 +375,7 @@ class ApplicationSetting < ApplicationRecord
if: :external_authorization_service_enabled
validates :spam_check_endpoint_url,
- addressable_url: true, allow_blank: true
+ addressable_url: { schemes: %w(grpc) }, allow_blank: true
validates :spam_check_endpoint_url,
presence: true,
@@ -434,6 +444,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :throttle_unauthenticated_packages_api_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_unauthenticated_packages_api_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :throttle_authenticated_api_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -450,6 +468,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :throttle_authenticated_packages_api_requests_per_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
+ validates :throttle_authenticated_packages_api_period_in_seconds,
+ presence: true,
+ numericality: { only_integer: true, greater_than: 0 }
+
validates :throttle_protected_paths_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -475,35 +501,43 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :whats_new_variant,
+ inclusion: { in: ApplicationSetting.whats_new_variants.keys }
+
+ validates :floc_enabled,
+ inclusion: { in: [true, false], message: _('must be a boolean value') }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc',
insecure_mode: true
- private_class_method def self.encryption_options_base_truncated_aes_256_gcm
+ private_class_method def self.encryption_options_base_32_aes_256_gcm
{
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: true
}
end
- attr_encrypted :external_auth_client_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :external_auth_client_key_pass, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :lets_encrypt_private_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :eks_secret_access_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :akismet_api_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :recaptcha_private_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm
- attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_truncated_aes_256_gcm
+ attr_encrypted :external_auth_client_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :external_auth_client_key_pass, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :lets_encrypt_private_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :eks_secret_access_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :akismet_api_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :spam_check_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false)
+ attr_encrypted :elasticsearch_aws_secret_access_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :elasticsearch_password, encryption_options_base_32_aes_256_gcm.merge(encode: false)
+ attr_encrypted :recaptcha_private_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :recaptcha_site_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :slack_app_secret, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :slack_app_verification_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :ci_jwt_signing_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :secret_detection_token_revocation_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :cloud_license_auth_token, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :external_pipeline_validation_service_token, encryption_options_base_32_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') }
@@ -634,4 +668,4 @@ class ApplicationSetting < ApplicationRecord
end
end
-ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')
+ApplicationSetting.prepend_mod_with('ApplicationSetting')
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 66a8d1f8105..5ff1c653f9e 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -38,6 +38,7 @@ module ApplicationSettingImplementation
admin_mode: false,
after_sign_up_text: nil,
akismet_enabled: false,
+ akismet_api_key: nil,
allow_local_requests_from_system_hooks: true,
allow_local_requests_from_web_hooks_and_services: false,
asset_proxy_enabled: false,
@@ -76,6 +77,7 @@ module ApplicationSettingImplementation
external_pipeline_validation_service_token: nil,
external_pipeline_validation_service_url: nil,
first_day_of_week: 0,
+ floc_enabled: false,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
@@ -149,6 +151,7 @@ module ApplicationSettingImplementation
sourcegraph_url: nil,
spam_check_endpoint_enabled: false,
spam_check_endpoint_url: nil,
+ spam_check_api_key: nil,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -156,6 +159,9 @@ module ApplicationSettingImplementation
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_web_requests_per_period: 7200,
+ throttle_authenticated_packages_api_enabled: false,
+ throttle_authenticated_packages_api_period_in_seconds: 15,
+ throttle_authenticated_packages_api_requests_per_period: 1000,
throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600,
@@ -165,6 +171,9 @@ module ApplicationSettingImplementation
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600,
+ throttle_unauthenticated_packages_api_enabled: false,
+ throttle_unauthenticated_packages_api_period_in_seconds: 15,
+ throttle_unauthenticated_packages_api_requests_per_period: 800,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
@@ -181,7 +190,8 @@ module ApplicationSettingImplementation
kroki_enabled: false,
kroki_url: nil,
kroki_formats: { blockdiag: false, bpmn: false, excalidraw: false },
- rate_limiting_response_text: nil
+ rate_limiting_response_text: nil,
+ whats_new_variant: 0
}
end
diff --git a/app/models/atlassian/identity.rb b/app/models/atlassian/identity.rb
index 906f2be0fbf..02bbe007e1b 100644
--- a/app/models/atlassian/identity.rb
+++ b/app/models/atlassian/identity.rb
@@ -11,14 +11,14 @@ module Atlassian
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
attr_encrypted :refresh_token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 32c9d44f836..aff7eef4622 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -92,4 +92,4 @@ class AuditEvent < ApplicationRecord
end
end
-AuditEvent.prepend_if_ee('EE::AuditEvent')
+AuditEvent.prepend_mod_with('AuditEvent')
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a851f22bfcd..a3801025cd7 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -33,7 +33,7 @@ module BlobViewer
@json_data ||= begin
prepare!
Gitlab::Json.parse(blob.data)
- rescue
+ rescue StandardError
{}
end
end
diff --git a/app/models/board.rb b/app/models/board.rb
index b26a9461ffc..7938819b6e4 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -45,6 +45,12 @@ class Board < ApplicationRecord
def to_type
self.class.to_type
end
+
+ def disabled_for?(current_user)
+ namespace = group_board? ? resource_parent.root_ancestor : resource_parent.root_namespace
+
+ namespace.issue_repositioning_disabled? || !Ability.allowed?(current_user, :create_non_backlog_issues, self)
+ end
end
-Board.prepend_if_ee('EE::Board')
+Board.prepend_mod_with('Board')
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index 979f0e1ab92..dc273e256a8 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific group a user has visited
class BoardGroupRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :group
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :group, presence: true
validates :board, presence: true
- scope :by_user_group, -> (user, group) { where(user: user, group: group) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, group: board.group, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, group, count: nil)
- visits = by_user_group(user, group).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
- visits.first(count)
+ def self.board_parent_relation
+ :group
end
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 509c8f97b83..723afd6feab 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific project a user has visited
class BoardProjectRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :project
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :project, presence: true
validates :board, presence: true
- scope :by_user_project, -> (user, project) { where(user: user, project: project) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, project: board.project, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, project, count: nil)
- visits = by_user_project(user, project).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
- visits.first(count)
+ def self.board_parent_relation
+ :project
end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index a8325e98095..1ee5c081840 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -106,6 +106,14 @@ class BroadcastMessage < ApplicationRecord
return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank?
+ # Ensure paths are consistent across callers.
+ # This fixes a mismatch between requests in the GUI and CLI
+ #
+ # This has to be reassigned due to frozen strings being provided.
+ unless current_path.start_with?("/")
+ current_path = "/#{current_path}"
+ end
+
escaped = Regexp.escape(target_path).gsub('\\*', '.*')
regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
@@ -119,4 +127,4 @@ class BroadcastMessage < ApplicationRecord
end
end
-BroadcastMessage.prepend_if_ee('EE::BroadcastMessage')
+BroadcastMessage.prepend_mod_with('BroadcastMessage')
diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb
index 4c6f745c268..6d9f598583e 100644
--- a/app/models/bulk_imports/configuration.rb
+++ b/app/models/bulk_imports/configuration.rb
@@ -12,11 +12,11 @@ class BulkImports::Configuration < ApplicationRecord
allow_nil: true
attr_encrypted :url,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm'
attr_encrypted :access_token,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm'
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 04af1145769..bb543b39a79 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -68,6 +68,10 @@ class BulkImports::Entity < ApplicationRecord
end
end
+ def encoded_source_full_path
+ ERB::Util.url_encode(source_full_path)
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb
new file mode 100644
index 00000000000..59ca4dbfec6
--- /dev/null
+++ b/app/models/bulk_imports/export.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class Export < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
+
+ self.table_name = 'bulk_import_exports'
+
+ belongs_to :project, optional: true
+ belongs_to :group, optional: true
+
+ has_one :upload, class_name: 'BulkImports::ExportUpload'
+
+ validates :project, presence: true, unless: :group
+ validates :group, presence: true, unless: :project
+ validates :relation, :status, presence: true
+
+ validate :portable_relation?
+
+ state_machine :status, initial: :started do
+ state :started, value: 0
+ state :finished, value: 1
+ state :failed, value: -1
+
+ event :start do
+ transition any => :started
+ end
+
+ event :finish do
+ transition started: :finished
+ transition failed: :failed
+ end
+
+ event :fail_op do
+ transition any => :failed
+ end
+ end
+
+ def portable_relation?
+ return unless portable
+
+ errors.add(:relation, 'Unsupported portable relation') unless config.portable_relations.include?(relation)
+ end
+
+ def portable
+ strong_memoize(:portable) do
+ project || group
+ end
+ end
+
+ def relation_definition
+ config.portable_tree[:include].find { |include| include[relation.to_sym] }
+ end
+
+ def config
+ strong_memoize(:config) do
+ FileTransfer.config_for(portable)
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb
new file mode 100644
index 00000000000..a9cba5119af
--- /dev/null
+++ b/app/models/bulk_imports/export_upload.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportUpload < ApplicationRecord
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ self.table_name = 'bulk_import_export_uploads'
+
+ belongs_to :export, class_name: 'BulkImports::Export'
+
+ mount_uploader :export_file, ExportUploader
+
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer.rb b/app/models/bulk_imports/file_transfer.rb
new file mode 100644
index 00000000000..5be954b98da
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ extend self
+
+ UnsupportedObjectType = Class.new(StandardError)
+
+ def config_for(portable)
+ case portable
+ when ::Project
+ FileTransfer::ProjectConfig.new(portable)
+ when ::Group
+ FileTransfer::GroupConfig.new(portable)
+ else
+ raise(UnsupportedObjectType, "Unsupported object type: #{portable.class}")
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/base_config.rb b/app/models/bulk_imports/file_transfer/base_config.rb
new file mode 100644
index 00000000000..bb04e84ad72
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/base_config.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class BaseConfig
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(portable)
+ @portable = portable
+ end
+
+ def portable_tree
+ attributes_finder.find_root(portable_class_sym)
+ end
+
+ def export_path
+ strong_memoize(:export_path) do
+ relative_path = File.join(base_export_path, SecureRandom.hex)
+
+ ::Gitlab::ImportExport.export_path(relative_path: relative_path)
+ end
+ end
+
+ def portable_relations
+ import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s)
+ end
+
+ private
+
+ attr_reader :portable
+
+ def attributes_finder
+ strong_memoize(:attributes_finder) do
+ ::Gitlab::ImportExport::AttributesFinder.new(config: import_export_config)
+ end
+ end
+
+ def import_export_config
+ ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h
+ end
+
+ def portable_class
+ @portable_class ||= portable.class
+ end
+
+ def portable_class_sym
+ @portable_class_sym ||= portable_class.to_s.demodulize.underscore.to_sym
+ end
+
+ def import_export_yaml
+ raise NotImplementedError
+ end
+
+ def base_export_path
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb
new file mode 100644
index 00000000000..1f845b387b8
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/group_config.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class GroupConfig < BaseConfig
+ def base_export_path
+ portable.full_path
+ end
+
+ def import_export_yaml
+ ::Gitlab::ImportExport.group_config_file
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb
new file mode 100644
index 00000000000..e42b5bfce3d
--- /dev/null
+++ b/app/models/bulk_imports/file_transfer/project_config.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module FileTransfer
+ class ProjectConfig < BaseConfig
+ def base_export_path
+ portable.disk_path
+ end
+
+ def import_export_yaml
+ ::Gitlab::ImportExport.config_file
+ end
+ end
+ end
+end
diff --git a/app/models/bulk_imports/stage.rb b/app/models/bulk_imports/stage.rb
deleted file mode 100644
index 050c2c76ce8..00000000000
--- a/app/models/bulk_imports/stage.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-module BulkImports
- class Stage
- include Singleton
-
- CONFIG = {
- group: {
- pipeline: BulkImports::Groups::Pipelines::GroupPipeline,
- stage: 0
- },
- subgroups: {
- pipeline: BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
- stage: 1
- },
- members: {
- pipeline: BulkImports::Groups::Pipelines::MembersPipeline,
- stage: 1
- },
- labels: {
- pipeline: BulkImports::Groups::Pipelines::LabelsPipeline,
- stage: 1
- },
- milestones: {
- pipeline: BulkImports::Groups::Pipelines::MilestonesPipeline,
- stage: 1
- },
- badges: {
- pipeline: BulkImports::Groups::Pipelines::BadgesPipeline,
- stage: 1
- },
- finisher: {
- pipeline: BulkImports::Groups::Pipelines::EntityFinisher,
- stage: 2
- }
- }.freeze
-
- def self.pipelines
- instance.pipelines
- end
-
- def self.pipeline_exists?(name)
- pipelines.any? do |(_, pipeline)|
- pipeline.to_s == name.to_s
- end
- end
-
- def pipelines
- @pipelines ||= config
- .values
- .sort_by { |entry| entry[:stage] }
- .map do |entry|
- [entry[:stage], entry[:pipeline]]
- end
- end
-
- private
-
- def config
- @config ||= CONFIG
- end
- end
-end
-
-::BulkImports::Stage.prepend_if_ee('::EE::BulkImports::Stage')
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 282ba9e19ac..1b108d5c042 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -35,7 +35,7 @@ class BulkImports::Tracker < ApplicationRecord
def pipeline_class
unless BulkImports::Stage.pipeline_exists?(pipeline_name)
- raise NameError.new("'#{pipeline_name}' is not a valid BulkImport Pipeline")
+ raise NameError, "'#{pipeline_name}' is not a valid BulkImport Pipeline"
end
pipeline_name.constantize
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index 0041595baba..ff3f2663b73 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -3,11 +3,11 @@
class ChatName < ApplicationRecord
LAST_USED_AT_INTERVAL = 1.hour
- belongs_to :service
+ belongs_to :integration, foreign_key: :service_id
belongs_to :user
validates :user, presence: true
- validates :service, presence: true
+ validates :integration, presence: true
validates :team_id, presence: true
validates :chat_id, presence: true
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index ca400cebe4e..352229c64da 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -163,6 +163,9 @@ module Ci
def expanded_environment_name
end
+ def instantized_environment
+ end
+
def execute_hooks
raise NotImplementedError
end
@@ -248,4 +251,4 @@ module Ci
end
end
-::Ci::Bridge.prepend_if_ee('::EE::Ci::Bridge')
+::Ci::Bridge.prepend_mod_with('Ci::Bridge')
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3d8e9f4c126..46fc87a6ea8 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -62,6 +62,9 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
+ ignore_columns :id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
+ ignore_columns :stage_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
+
##
# Since Gitlab 11.5, deployments records started being created right after
# `ci_builds` creation. We can look up a relevant `environment` through
@@ -85,6 +88,16 @@ module Ci
end
end
+ # Initializing an object instead of fetching `persisted_environment` for avoiding unnecessary queries.
+ # We're planning to introduce a direct relationship between build and environment
+ # in https://gitlab.com/gitlab-org/gitlab/-/issues/326445 to let us to preload
+ # in batch.
+ def instantized_environment
+ return unless has_environment?
+
+ ::Environment.new(project: self.project, name: self.expanded_environment_name)
+ end
+
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
@@ -330,7 +343,7 @@ module Ci
begin
build.deployment.drop!
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
end
@@ -1047,7 +1060,7 @@ module Ci
end
def build_data
- @build_data ||= Gitlab::DataBuilder::Build.build(self)
+ strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end
def successful_deployment_status
@@ -1141,4 +1154,4 @@ module Ci
end
end
-Ci::Build.prepend_if_ee('EE::Ci::Build')
+Ci::Build.prepend_mod_with('Ci::Build')
diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb
index 8ae921f1416..716d919487d 100644
--- a/app/models/ci/build_dependencies.rb
+++ b/app/models/ci/build_dependencies.rb
@@ -14,14 +14,33 @@ module Ci
(local + cross_pipeline + cross_project).uniq
end
+ def invalid_local
+ local.reject(&:valid_dependency?)
+ end
+
+ def valid?
+ valid_local? && valid_cross_pipeline? && valid_cross_project?
+ end
+
+ private
+
+ # Dependencies can only be of Ci::Build type because only builds
+ # can create artifacts
+ def model_class
+ ::Ci::Build
+ end
+
# Dependencies local to the given pipeline
def local
- return [] if no_local_dependencies_specified?
-
- deps = model_class.where(pipeline_id: processable.pipeline_id).latest
- deps = from_previous_stages(deps)
- deps = from_needs(deps)
- from_dependencies(deps)
+ strong_memoize(:local) do
+ next [] if no_local_dependencies_specified?
+ next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline
+
+ deps = model_class.where(pipeline_id: processable.pipeline_id).latest
+ deps = from_previous_stages(deps)
+ deps = from_needs(deps)
+ from_dependencies(deps).to_a
+ end
end
# Dependencies from the same parent-pipeline hierarchy excluding
@@ -37,22 +56,6 @@ module Ci
[]
end
- def invalid_local
- local.reject(&:valid_dependency?)
- end
-
- def valid?
- valid_local? && valid_cross_pipeline? && valid_cross_project?
- end
-
- private
-
- # Dependencies can only be of Ci::Build type because only builds
- # can create artifacts
- def model_class
- ::Ci::Build
- end
-
def fetch_dependencies_in_hierarchy
deps_specifications = specified_cross_pipeline_dependencies
return [] if deps_specifications.empty?
@@ -102,8 +105,6 @@ module Ci
end
def valid_local?
- return true unless Gitlab::Ci::Features.validate_build_dependencies?(project)
-
local.all?(&:valid_dependency?)
end
@@ -154,4 +155,4 @@ module Ci
end
end
-Ci::BuildDependencies.prepend_if_ee('EE::Ci::BuildDependencies')
+Ci::BuildDependencies.prepend_mod_with('Ci::BuildDependencies')
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 7bc70f9f1e1..4a59c25cbb0 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -5,6 +5,9 @@ module Ci
extend Gitlab::Ci::Model
include BulkInsertSafe
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
@@ -14,5 +17,12 @@ module Ci
scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
scope :artifacts, -> { where(artifacts: true) }
+
+ # TODO: Remove once build_id_convert_to_bigint is not an "ignored" column anymore (see .ignore_columns above)
+ # There is a database-side trigger to populate this column. This is unexpected in the context
+ # of cloning an instance, e.g. when retrying the job. Hence we exclude the ignored column explicitly here.
+ def attributes
+ super.except('build_id_convert_to_bigint')
+ end
end
end
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index b6196048ca1..2aa856dbc64 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -5,6 +5,9 @@ module Ci
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < ApplicationRecord
extend Gitlab::Ci::Model
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7e03d709f24..719511bbb8a 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -8,6 +8,9 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb
index 7f952fb77a0..dde4b534aaa 100644
--- a/app/models/ci/commit_with_pipeline.rb
+++ b/app/models/ci/commit_with_pipeline.rb
@@ -18,9 +18,25 @@ class Ci::CommitWithPipeline < SimpleDelegator
end
end
+ def lazy_latest_pipeline
+ BatchLoader.for(sha).batch do |shas, loader|
+ preload_pipelines = project.ci_pipelines.latest_pipeline_per_commit(shas.compact)
+
+ shas.each do |sha|
+ pipeline = preload_pipelines[sha]
+
+ loader.call(sha, pipeline)
+ end
+ end
+ end
+
def latest_pipeline(ref = nil)
@latest_pipelines.fetch(ref) do |ref|
- @latest_pipelines[ref] = latest_pipeline_for_project(ref, project)
+ @latest_pipelines[ref] = if ref
+ latest_pipeline_for_project(ref, project)
+ else
+ lazy_latest_pipeline&.itself
+ end
end
end
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index 5dcf575abd7..b46d32474c6 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -30,4 +30,4 @@ module Ci
end
end
-Ci::DailyBuildGroupReportResult.prepend_if_ee('EE::Ci::DailyBuildGroupReportResult')
+Ci::DailyBuildGroupReportResult.prepend_mod_with('Ci::DailyBuildGroupReportResult')
diff --git a/app/models/ci/deleted_object.rb b/app/models/ci/deleted_object.rb
index 2942a153e05..b2a949c9bb5 100644
--- a/app/models/ci/deleted_object.rb
+++ b/app/models/ci/deleted_object.rb
@@ -29,7 +29,7 @@ module Ci
def delete_file_from_storage
file.remove!
true
- rescue => exception
+ rescue StandardError => exception
Gitlab::ErrorTracking.track_exception(exception)
false
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 50e21a1c323..5248a80f710 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -261,6 +261,22 @@ module Ci
self.where(project: project).sum(:size)
end
+ ##
+ # FastDestroyAll concerns
+ # rubocop: disable CodeReuse/ServiceClass
+ def self.begin_fast_destroy
+ service = ::Ci::JobArtifacts::DestroyAssociationsService.new(self)
+ service.destroy_records
+ service
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
+ ##
+ # FastDestroyAll concerns
+ def self.finalize_fast_destroy(service)
+ service.update_statistics
+ end
+
def local_store?
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
@@ -331,4 +347,4 @@ module Ci
end
end
-Ci::JobArtifact.prepend_if_ee('EE::Ci::JobArtifact')
+Ci::JobArtifact.prepend_mod_with('Ci::JobArtifact')
diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb
index 91163c85a9e..57aa1962bd2 100644
--- a/app/models/ci/persistent_ref.rb
+++ b/app/models/ci/persistent_ref.rb
@@ -15,13 +15,13 @@ module Ci
def exist?
ref_exists?(path)
- rescue
+ rescue StandardError
false
end
def create
create_ref(sha, path)
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking
.track_exception(e, pipeline_id: pipeline.id)
end
@@ -30,7 +30,7 @@ module Ci
delete_refs(path)
rescue Gitlab::Git::Repository::NoRepository
# no-op
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking
.track_exception(e, pipeline_id: pipeline.id)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c9ab69317e1..f0a2c074584 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -17,6 +17,7 @@ module Ci
include FromUnion
include UpdatedAtFilterable
include EachBatch
+ include FastDestroyAll::Helpers
MAX_OPEN_MERGE_REQUESTS_REFS = 4
@@ -70,7 +71,9 @@ module Ci
has_many :deployments, through: :builds
has_many :environments, -> { distinct }, through: :deployments
has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
- has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts
+ has_many :downloadable_artifacts, -> do
+ not_expired.or(where_exists(::Ci::Pipeline.artifacts_locked.where('ci_pipelines.id = ci_builds.commit_id'))).downloadable.with_job
+ end, through: :latest_builds, source: :job_artifacts
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
@@ -124,6 +127,8 @@ module Ci
after_create :keep_around_commits, unless: :importing?
+ use_fast_destroy :job_artifacts
+
# We use `Enums::Ci::Pipeline.sources` here so that EE can more easily extend
# this `Hash` with new values.
enum_with_nil source: Enums::Ci::Pipeline.sources
@@ -908,7 +913,7 @@ module Ci
def same_family_pipeline_ids
::Gitlab::Ci::PipelineObjectHierarchy.new(
- self.class.where(id: root_ancestor), options: { same_project: true }
+ self.class.default_scoped.where(id: root_ancestor), options: { same_project: true }
).base_and_descendants.select(:id)
end
@@ -1093,6 +1098,8 @@ module Ci
merge_request.modified_paths
elsif branch_updated?
push_details.modified_paths
+ elsif external_pull_request? && ::Feature.enabled?(:ci_modified_paths_of_external_prs, project, default_enabled: :yaml)
+ external_pull_request.modified_paths
end
end
end
@@ -1117,6 +1124,10 @@ module Ci
merge_request_id.present?
end
+ def external_pull_request?
+ external_pull_request_id.present?
+ end
+
def detached_merge_request_pipeline?
merge_request? && target_sha.nil?
end
@@ -1210,11 +1221,18 @@ module Ci
# We need `base_and_ancestors` in a specific order to "break" when needed.
# If we use `find_each`, then the order is broken.
# rubocop:disable Rails/FindEach
- def reset_ancestor_bridges!
- base_and_ancestors.includes(:source_bridge).each do |pipeline|
- break unless pipeline.bridge_waiting?
+ def reset_source_bridge!(current_user)
+ if ::Feature.enabled?(:ci_reset_bridge_with_subsequent_jobs, project, default_enabled: :yaml)
+ return unless bridge_waiting?
- pipeline.source_bridge.pending!
+ source_bridge.pending!
+ Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
+ else
+ base_and_ancestors.includes(:source_bridge).each do |pipeline|
+ break unless pipeline.bridge_waiting?
+
+ pipeline.source_bridge.pending!
+ end
end
end
# rubocop:enable Rails/FindEach
@@ -1237,8 +1255,6 @@ module Ci
private
def add_message(severity, content)
- return unless Gitlab::Ci::Features.store_pipeline_messages?(project)
-
messages.build(severity: severity, content: content)
end
@@ -1294,4 +1310,4 @@ module Ci
end
end
-Ci::Pipeline.prepend_if_ee('EE::Ci::Pipeline')
+Ci::Pipeline.prepend_mod_with('Ci::Pipeline')
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 9dfe4252e95..889c5d094a7 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -40,6 +40,8 @@ module Ci
code_quality_mr_diff: 2
}
+ scope :unlocked, -> { joins(:pipeline).merge(::Ci::Pipeline.unlocked) }
+
class << self
def report_exists?(file_type)
return false unless REPORT_TYPES.key?(file_type)
@@ -58,4 +60,4 @@ module Ci
end
end
-Ci::PipelineArtifact.prepend_ee_mod
+Ci::PipelineArtifact.prepend_mod
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 3c17246bc34..9e5d517c1fe 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -5,7 +5,7 @@ module Ci
extend Gitlab::Ci::Model
include Importable
include StripAttribute
- include Schedulable
+ include CronSchedulable
include Limitable
include EachBatch
@@ -51,38 +51,16 @@ module Ci
update_attribute(:active, false)
end
- ##
- # The `next_run_at` column is set to the actual execution date of `PipelineScheduleWorker`.
- # This way, a schedule like `*/1 * * * *` won't be triggered in a short interval
- # when PipelineScheduleWorker runs irregularly by Sidekiq Memory Killer.
- def set_next_run_at
- now = Time.zone.now
- ideal_next_run = ideal_next_run_from(now)
-
- self.next_run_at = if ideal_next_run == cron_worker_next_run_from(now)
- ideal_next_run
- else
- cron_worker_next_run_from(ideal_next_run)
- end
- end
-
def job_variables
variables&.map(&:to_runner_variable) || []
end
private
- def ideal_next_run_from(start_time)
- Gitlab::Ci::CronParser.new(cron, cron_timezone)
- .next_time_from(start_time)
- end
-
- def cron_worker_next_run_from(start_time)
- Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'],
- Time.zone.name)
- .next_time_from(start_time)
+ def worker_cron_expression
+ Settings.cron_jobs['pipeline_schedule_worker']['cron']
end
end
end
-Ci::PipelineSchedule.prepend_if_ee('EE::Ci::PipelineSchedule')
+Ci::PipelineSchedule.prepend_mod_with('Ci::PipelineSchedule')
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 3b61840805a..15c57550159 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -120,6 +120,10 @@ module Ci
raise NotImplementedError
end
+ def instantized_environment
+ raise NotImplementedError
+ end
+
override :all_met_to_become_pending?
def all_met_to_become_pending?
super && !with_resource_group?
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 05126853e0f..8c877c2b818 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -39,16 +39,16 @@ module Ci
AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze
AVAILABLE_TYPES = runner_types.keys.freeze
- AVAILABLE_STATUSES = %w[active paused online offline].freeze
+ AVAILABLE_STATUSES = %w[active paused online offline not_connected].freeze
AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
MINUTES_COST_FACTOR_FIELDS = %i[public_projects_minutes_cost_factor private_projects_minutes_cost_factor].freeze
has_many :builds
- has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
- has_many :runner_namespaces, inverse_of: :runner
+ has_many :runner_namespaces, inverse_of: :runner, autosave: true
has_many :groups, through: :runner_namespaces
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
@@ -65,6 +65,7 @@ module Ci
# did `contacted_at <= ?` the query would effectively have to do a seq
# scan.
scope :offline, -> { where.not(id: online) }
+ scope :not_connected, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
@@ -405,4 +406,4 @@ module Ci
end
end
-Ci::Runner.prepend_if_ee('EE::Ci::Runner')
+Ci::Runner.prepend_mod_with('Ci::Runner')
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index e6c1899c89d..f819dda207d 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -3,6 +3,11 @@
module Ci
class RunnerNamespace < ApplicationRecord
extend Gitlab::Ci::Model
+ include Limitable
+
+ self.limit_name = 'ci_registered_group_runners'
+ self.limit_scope = :group
+ self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_namespaces
belongs_to :namespace, inverse_of: :runner_namespaces, class_name: '::Namespace'
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index f5bd50dc5a3..c26b8183b52 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -3,6 +3,11 @@
module Ci
class RunnerProject < ApplicationRecord
extend Gitlab::Ci::Model
+ include Limitable
+
+ self.limit_name = 'ci_registered_project_runners'
+ self.limit_scope = :project
+ self.limit_feature_flag = :ci_runner_limits
belongs_to :runner, inverse_of: :runner_projects
belongs_to :project, inverse_of: :runner_projects
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 5ae97dcd495..ef920b2d589 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -41,7 +41,7 @@ module Ci
self.position = statuses.select(:stage_idx)
.where.not(stage_idx: nil)
.group(:stage_idx)
- .order('COUNT(*) DESC')
+ .order('COUNT(id) DESC')
.first&.stage_idx.to_i
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 85cb3f5b46a..6e27abb9f5b 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -37,4 +37,4 @@ module Ci
end
end
-Ci::Trigger.prepend_if_ee('EE::Ci::Trigger')
+Ci::Trigger.prepend_mod_with('Ci::Trigger')
diff --git a/app/models/ci/unit_test.rb b/app/models/ci/unit_test.rb
index 81623b4f6ad..9fddd9c6002 100644
--- a/app/models/ci/unit_test.rb
+++ b/app/models/ci/unit_test.rb
@@ -14,6 +14,7 @@ module Ci
belongs_to :project
scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) }
+ scope :deletable, -> { where('NOT EXISTS (?)', Ci::UnitTestFailure.select(1).where("#{Ci::UnitTestFailure.table_name}.unit_test_id = #{table_name}.id")) }
class << self
def find_or_create_by_batch(project, unit_test_attrs)
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index 653a56bd2b3..480f9cefb8e 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -11,6 +11,8 @@ module Ci
belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ scope :deletable, -> { where('failed_at < ?', REPORT_WINDOW.ago) }
+
def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current)
joins(:unit_test)
.where(
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index c5b9dddb1da..9fb8cd024c5 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -8,6 +8,7 @@ module Clusters
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
+ has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index d42279502c5..27a3cd8d13d 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -6,7 +6,7 @@ module Clusters
include TokenAuthenticatable
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
- cached_attr_reader :last_contacted_at
+ cached_attr_reader :last_used_at
self.table_name = 'cluster_agent_tokens'
@@ -21,6 +21,8 @@ module Clusters
validates :description, length: { maximum: 1024 }
validates :name, presence: true, length: { maximum: 255 }
+ scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
+
def track_usage
track_values = { last_used_at: Time.current.utc }
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
index db18a29ec84..73c731aab1a 100644
--- a/app/models/clusters/applications/elastic_stack.rb
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -3,9 +3,9 @@
module Clusters
module Applications
class ElasticStack < ApplicationRecord
- VERSION = '3.0.0'
+ include ::Clusters::Concerns::ElasticsearchClient
- ELASTICSEARCH_PORT = 9200
+ VERSION = '3.0.0'
self.table_name = 'clusters_applications_elastic_stacks'
@@ -13,10 +13,23 @@ module Clusters
include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
- include ::Gitlab::Utils::StrongMemoize
default_value_for :version, VERSION
+ after_destroy do
+ cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
+ end
+
+ state_machine :status do
+ after_transition any => [:installed] do |application|
+ application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: true, chart_version: application.version)
+ end
+
+ after_transition any => [:uninstalled] do |application|
+ application.cluster&.find_or_build_integration_elastic_stack&.update(enabled: false, chart_version: nil)
+ end
+ end
+
def chart
'elastic-stack/elastic-stack'
end
@@ -51,31 +64,6 @@ module Clusters
super.merge('wait-for-elasticsearch.sh': File.read("#{Rails.root}/vendor/elastic_stack/wait-for-elasticsearch.sh"))
end
- def elasticsearch_client(timeout: nil)
- strong_memoize(:elasticsearch_client) do
- next unless kube_client
-
- proxy_url = kube_client.proxy_url('service', service_name, ::Clusters::Applications::ElasticStack::ELASTICSEARCH_PORT, Gitlab::Kubernetes::Helm::NAMESPACE)
-
- Elasticsearch::Client.new(url: proxy_url) do |faraday|
- # ensures headers containing auth data are appended to original client options
- faraday.headers.merge!(kube_client.headers)
- # ensure TLS certs are properly verified
- faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
- faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
- faraday.options.timeout = timeout unless timeout.nil?
- end
-
- rescue Kubeclient::HttpError => error
- # If users have mistakenly set parameters or removed the depended clusters,
- # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
- # We check for a nil client in downstream use and behaviour is equivalent to an empty state
- log_exception(error, :failed_to_create_elasticsearch_client)
-
- nil
- end
- end
-
def chart_above_v2?
Gem::Version.new(version) >= Gem::Version.new('2.0.0')
end
@@ -106,10 +94,6 @@ module Clusters
]
end
- def kube_client
- cluster&.kubeclient&.core_client
- end
-
def migrate_to_3_script
return [] if !updating? || chart_above_v3?
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index b9c136abab4..21f7e410843 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -22,21 +22,18 @@ module Clusters
attr_encrypted :alert_manager_token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
+ default_value_for(:alert_manager_token) { SecureRandom.hex }
+
after_destroy do
- run_after_commit do
- disable_prometheus_integration
- end
+ cluster.find_or_build_integration_prometheus.destroy
end
state_machine :status do
after_transition any => [:installed, :externally_installed] do |application|
- application.run_after_commit do
- Clusters::Applications::ActivateServiceWorker
- .perform_async(application.cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
- end
+ application.cluster.find_or_build_integration_prometheus.update(enabled: true, alert_manager_token: application.alert_manager_token)
end
after_transition any => :updating do |application|
@@ -44,6 +41,10 @@ module Clusters
end
end
+ def managed_prometheus?
+ !externally_installed? && !uninstalled?
+ end
+
def updated_since?(timestamp)
last_update_started_at &&
last_update_started_at > timestamp &&
@@ -70,6 +71,7 @@ module Clusters
)
end
+ # Deprecated, to be removed in %14.0 as part of https://gitlab.com/groups/gitlab-org/-/epics/4280
def patch_command(values)
helm_command_module::PatchCommand.new(
name: name,
@@ -98,23 +100,8 @@ module Clusters
files.merge('values.yaml': replaced_values)
end
- def generate_alert_manager_token!
- unless alert_manager_token.present?
- update!(alert_manager_token: generate_token)
- end
- end
-
private
- def generate_token
- SecureRandom.hex
- end
-
- def disable_prometheus_integration
- ::Clusters::Applications::DeactivateServiceWorker
- .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
- end
-
def install_knative_metrics
return [] unless cluster.application_knative_available?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index bc80bcd0b06..e8d56072b89 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.27.0'
+ VERSION = '0.28.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index a1e2aa194a0..4877ced795c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -52,6 +52,7 @@ module Clusters
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', inverse_of: :cluster, autosave: true
has_one :integration_prometheus, class_name: 'Clusters::Integrations::Prometheus', inverse_of: :cluster
+ has_one :integration_elastic_stack, class_name: 'Clusters::Integrations::ElasticStack', inverse_of: :cluster
def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateName
application = APPLICATIONS[name.to_s]
@@ -104,6 +105,7 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_knative, prefix: true, allow_nil: true
delegate :available?, to: :application_elastic_stack, prefix: true, allow_nil: true
+ delegate :available?, to: :integration_elastic_stack, prefix: true, allow_nil: true
delegate :external_ip, to: :application_ingress, prefix: true, allow_nil: true
delegate :external_hostname, to: :application_ingress, prefix: true, allow_nil: true
@@ -284,6 +286,10 @@ module Clusters
integration_prometheus || build_integration_prometheus
end
+ def find_or_build_integration_elastic_stack
+ integration_elastic_stack || build_integration_elastic_stack
+ end
+
def provider
if gcp?
provider_gcp
@@ -318,6 +324,22 @@ module Clusters
platform_kubernetes.kubeclient if kubernetes?
end
+ def elastic_stack_adapter
+ application_elastic_stack || integration_elastic_stack
+ end
+
+ def elasticsearch_client
+ elastic_stack_adapter&.elasticsearch_client
+ end
+
+ def elastic_stack_available?
+ if application_elastic_stack_available? || integration_elastic_stack_available?
+ true
+ else
+ false
+ end
+ end
+
def kubernetes_namespace_for(environment, deployable: environment.last_deployable)
if deployable && environment.project_id != deployable.project_id
raise ArgumentError, 'environment.project_id must match deployable.project_id'
@@ -470,4 +492,4 @@ module Clusters
end
end
-Clusters::Cluster.prepend_if_ee('EE::Clusters::Cluster')
+Clusters::Cluster.prepend_mod_with('Clusters::Cluster')
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index ad6699daa78..2e40689a650 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -6,6 +6,8 @@ module Clusters
extend ActiveSupport::Concern
included do
+ include ::Clusters::Concerns::KubernetesLogger
+
belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
validates :cluster, presence: true
@@ -79,27 +81,9 @@ module Clusters
# Override if your application needs any action after
# being uninstalled by Helm
end
-
- def logger
- @logger ||= Gitlab::Kubernetes::Logger.build
- end
-
- def log_exception(error, event)
- logger.error({
- exception: error.class.name,
- status_code: error.error_code,
- cluster_id: cluster&.id,
- application_id: id,
- class_name: self.class.name,
- event: event,
- message: error.message
- })
-
- Gitlab::ErrorTracking.track_exception(error, cluster_id: cluster&.id, application_id: id)
- end
end
end
end
end
-Clusters::Concerns::ApplicationCore.prepend_if_ee('EE::Clusters::Concerns::ApplicationCore')
+Clusters::Concerns::ApplicationCore.prepend_mod_with('Clusters::Concerns::ApplicationCore')
diff --git a/app/models/clusters/concerns/elasticsearch_client.rb b/app/models/clusters/concerns/elasticsearch_client.rb
new file mode 100644
index 00000000000..7b0b6bdae02
--- /dev/null
+++ b/app/models/clusters/concerns/elasticsearch_client.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Concerns
+ module ElasticsearchClient
+ include ::Gitlab::Utils::StrongMemoize
+
+ ELASTICSEARCH_PORT = 9200
+ ELASTICSEARCH_NAMESPACE = 'gitlab-managed-apps'
+
+ def elasticsearch_client(timeout: nil)
+ strong_memoize(:elasticsearch_client) do
+ kube_client = cluster&.kubeclient&.core_client
+ next unless kube_client
+
+ proxy_url = kube_client.proxy_url('service', service_name, ELASTICSEARCH_PORT, ELASTICSEARCH_NAMESPACE)
+
+ Elasticsearch::Client.new(url: proxy_url) do |faraday|
+ # ensures headers containing auth data are appended to original client options
+ faraday.headers.merge!(kube_client.headers)
+ # ensure TLS certs are properly verified
+ faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
+ faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
+ faraday.options.timeout = timeout unless timeout.nil?
+ end
+
+ rescue Kubeclient::HttpError => error
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # We check for a nil client in downstream use and behaviour is equivalent to an empty state
+ log_exception(error, :failed_to_create_elasticsearch_client)
+
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/concerns/kubernetes_logger.rb b/app/models/clusters/concerns/kubernetes_logger.rb
new file mode 100644
index 00000000000..2eca33a7610
--- /dev/null
+++ b/app/models/clusters/concerns/kubernetes_logger.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Concerns
+ module KubernetesLogger
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_exception(error, event)
+ logger.error(
+ {
+ exception: error.class.name,
+ status_code: error.error_code,
+ cluster_id: cluster&.id,
+ application_id: id,
+ class_name: self.class.name,
+ event: event,
+ message: error.message
+ }
+ )
+
+ Gitlab::ErrorTracking.track_exception(error, cluster_id: cluster&.id, application_id: id)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb
new file mode 100644
index 00000000000..565d268259a
--- /dev/null
+++ b/app/models/clusters/integrations/elastic_stack.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Integrations
+ class ElasticStack < ApplicationRecord
+ include ::Clusters::Concerns::ElasticsearchClient
+ include ::Clusters::Concerns::KubernetesLogger
+
+ self.table_name = 'clusters_integration_elasticstack'
+ self.primary_key = :cluster_id
+
+ belongs_to :cluster, class_name: 'Clusters::Cluster', foreign_key: :cluster_id
+
+ validates :cluster, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+
+ def available?
+ enabled
+ end
+
+ def service_name
+ chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
+ end
+
+ def chart_above_v2?
+ return true if chart_version.nil?
+
+ Gem::Version.new(chart_version) >= Gem::Version.new('2.0.0')
+ end
+
+ def chart_above_v3?
+ return true if chart_version.nil?
+
+ Gem::Version.new(chart_version) >= Gem::Version.new('3.0.0')
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 1496d8ff1dd..0a01ac5d1ce 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -4,6 +4,7 @@ module Clusters
module Integrations
class Prometheus < ApplicationRecord
include ::Clusters::Concerns::PrometheusClient
+ include AfterCommitQueue
self.table_name = 'clusters_integration_prometheus'
self.primary_key = :cluster_id
@@ -13,9 +14,46 @@ module Clusters
validates :cluster, presence: true
validates :enabled, inclusion: { in: [true, false] }
+ attr_encrypted :alert_manager_token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm'
+
+ default_value_for(:alert_manager_token) { SecureRandom.hex }
+
+ after_destroy do
+ run_after_commit do
+ deactivate_project_services
+ end
+ end
+
+ after_save do
+ next unless enabled_before_last_save != enabled
+
+ run_after_commit do
+ if enabled
+ activate_project_services
+ else
+ deactivate_project_services
+ end
+ end
+ end
+
def available?
enabled?
end
+
+ private
+
+ def activate_project_services
+ ::Clusters::Applications::ActivateServiceWorker
+ .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
+ end
+
+ def deactivate_project_services
+ ::Clusters::Applications::DeactivateServiceWorker
+ .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
+ end
end
end
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index bfd01775620..af2eba42721 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -18,7 +18,7 @@ module Clusters
attr_encrypted :secret_access_key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
validates :role_arn,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5c3e3685c64..09e43bb8f20 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -142,6 +142,7 @@ class Commit
delegate \
:pipelines,
:last_pipeline,
+ :lazy_latest_pipeline,
:latest_pipeline,
:latest_pipeline_for_project,
:set_latest_pipeline_for_ref,
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e989129209a..c5ba19438cd 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -214,8 +214,14 @@ class CommitStatus < ApplicationRecord
allow_failure? && (failed? || canceled?)
end
+ # Time spent running.
def duration
- calculate_duration
+ calculate_duration(started_at, finished_at)
+ end
+
+ # Time spent in the pending state.
+ def queued_duration
+ calculate_duration(queued_at, started_at)
end
def latest?
@@ -286,4 +292,4 @@ class CommitStatus < ApplicationRecord
end
end
-CommitStatus.prepend_if_ee('::EE::CommitStatus')
+CommitStatus.prepend_mod_with('CommitStatus')
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index f1c39dda49d..90d48aa81d0 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -27,6 +27,7 @@ module Analytics
scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
+ scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
end
def parent=(_)
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index bbf9ecbcfe9..80cf6260b0b 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -214,9 +214,9 @@ module AtomicInternalId
def self.project_init(klass, column_name = :iid)
->(instance, scope) do
if instance
- klass.where(project_id: instance.project_id).maximum(column_name)
+ klass.default_scoped.where(project_id: instance.project_id).maximum(column_name)
elsif scope.present?
- klass.where(**scope).maximum(column_name)
+ klass.default_scoped.where(**scope).maximum(column_name)
end
end
end
diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb
new file mode 100644
index 00000000000..fd4d574ac58
--- /dev/null
+++ b/app/models/concerns/board_recent_visit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module BoardRecentVisit
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def visited!(user, board)
+ find_or_create_by(
+ "user" => user,
+ board_parent_relation => board.resource_parent,
+ board_relation => board
+ ).tap do |visit|
+ visit.touch
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def latest(user, parent, count: nil)
+ visits = by_user_parent(user, parent).order(updated_at: :desc)
+ visits = visits.preload(board_relation)
+
+ visits.first(count)
+ end
+
+ def board_relation
+ :board
+ end
+
+ def board_parent_relation
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 34c1b6d25a4..a5cf947ba07 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -26,7 +26,7 @@ module CacheMarkdownField
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
- raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ raise ArgumentError, "Unknown field: #{field.inspect}" unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains
@@ -99,7 +99,7 @@ module CacheMarkdownField
end
def cached_html_for(markdown_field)
- raise ArgumentError.new("Unknown field: #{markdown_field}") unless
+ raise ArgumentError, "Unknown field: #{markdown_field}" unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index ee56322cce7..f3b47047c55 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -53,7 +53,7 @@ module CacheableAttributes
return cached_record if cached_record.present?
current_without_cache.tap { |current_record| current_record&.cache! }
- rescue => e
+ rescue StandardError => e
if Rails.env.production?
Gitlab::AppLogger.warn("Cached record for #{name} couldn't be loaded, falling back to uncached record: #{e}")
else
@@ -66,7 +66,7 @@ module CacheableAttributes
def expire
Gitlab::SafeRequestStore.delete(request_store_cache_key)
cache_backend.delete(cache_key)
- rescue
+ rescue StandardError
# Gracefully handle when Redis is not available. For example,
# omnibus may fail here during gitlab:assets:compile.
end
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index 2b4a108a9a0..9efd90756b1 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -55,6 +55,7 @@ module CascadingNamespaceSettingAttribute
# public methods
define_attr_reader(attribute)
define_attr_writer(attribute)
+ define_lock_attr_writer(attribute)
define_lock_methods(attribute)
alias_boolean(attribute)
@@ -84,7 +85,7 @@ module CascadingNamespaceSettingAttribute
next self[attribute] unless self.class.cascading_settings_feature_enabled?
next self[attribute] if will_save_change_to_attribute?(attribute)
- next locked_value(attribute) if cascading_attribute_locked?(attribute)
+ next locked_value(attribute) if cascading_attribute_locked?(attribute, include_self: false)
next self[attribute] unless self[attribute].nil?
cascaded_value = cascaded_ancestor_value(attribute)
@@ -97,15 +98,25 @@ module CascadingNamespaceSettingAttribute
def define_attr_writer(attribute)
define_method("#{attribute}=") do |value|
+ return value if value == cascaded_ancestor_value(attribute)
+
clear_memoization(attribute)
+ super(value)
+ end
+ end
+
+ def define_lock_attr_writer(attribute)
+ define_method("lock_#{attribute}=") do |value|
+ attr_value = public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ write_attribute(attribute, attr_value) if self[attribute].nil?
super(value)
end
end
def define_lock_methods(attribute)
- define_method("#{attribute}_locked?") do
- cascading_attribute_locked?(attribute)
+ define_method("#{attribute}_locked?") do |include_self: false|
+ cascading_attribute_locked?(attribute, include_self: include_self)
end
define_method("#{attribute}_locked_by_ancestor?") do
@@ -133,7 +144,7 @@ module CascadingNamespaceSettingAttribute
def define_validator_methods(attribute)
define_method("#{attribute}_changeable?") do
return unless cascading_attribute_changed?(attribute)
- return unless cascading_attribute_locked?(attribute)
+ return unless cascading_attribute_locked?(attribute, include_self: false)
errors.add(attribute, s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
@@ -141,7 +152,7 @@ module CascadingNamespaceSettingAttribute
define_method("lock_#{attribute}_changeable?") do
return unless cascading_attribute_changed?("lock_#{attribute}")
- if cascading_attribute_locked?(attribute)
+ if cascading_attribute_locked?(attribute, include_self: false)
return errors.add(:"lock_#{attribute}", s_('CascadingSettings|cannot be changed because it is locked by an ancestor'))
end
@@ -202,8 +213,9 @@ module CascadingNamespaceSettingAttribute
Gitlab::CurrentSettings.public_send("lock_#{attribute}") # rubocop:disable GitlabSecurity/PublicSend
end
- def cascading_attribute_locked?(attribute)
- locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
+ def cascading_attribute_locked?(attribute, include_self:)
+ locked_by_self = include_self ? public_send("lock_#{attribute}?") : false # rubocop:disable GitlabSecurity/PublicSend
+ locked_by_self || locked_by_ancestor?(attribute) || locked_by_application_setting?(attribute)
end
def cascading_attribute_changed?(attribute)
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
index 0d29955268f..27040a677ff 100644
--- a/app/models/concerns/ci/artifactable.rb
+++ b/app/models/concerns/ci/artifactable.rb
@@ -43,4 +43,4 @@ module Ci
end
end
-Ci::Artifactable.prepend_ee_mod
+Ci::Artifactable.prepend_mod
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index c990da5873a..f3c254053b5 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -122,12 +122,10 @@ module Ci
private
- def calculate_duration
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.current - started_at
- end
+ def calculate_duration(start_time, end_time)
+ return unless start_time
+
+ (end_time || Time.current) - start_time
end
end
end
diff --git a/app/models/concerns/ci/maskable.rb b/app/models/concerns/ci/maskable.rb
index 4e0ee72f18f..e1ef4531845 100644
--- a/app/models/concerns/ci/maskable.rb
+++ b/app/models/concerns/ci/maskable.rb
@@ -9,9 +9,9 @@ module Ci
# * No variables
# * No spaces
# * Minimal length of 8 characters
- # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'
+ # * Characters must be from the Base64 alphabet (RFC4648) with the addition of '@', ':', '.', and '~'
# * Absolutely no fun is allowed
- REGEX = /\A[a-zA-Z0-9_+=\/@:.-]{8,}\z/.freeze
+ REGEX = /\A[a-zA-Z0-9_+=\/@:.~-]{8,}\z/.freeze
included do
validates :masked, inclusion: { in: [true, false] }
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 26e644646b4..601637ea32a 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -88,4 +88,4 @@ module Ci
end
end
-Ci::Metadatable.prepend_if_ee('EE::Ci::Metadatable')
+Ci::Metadatable.prepend_mod_with('Ci::Metadatable')
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
new file mode 100644
index 00000000000..beb3a09c119
--- /dev/null
+++ b/app/models/concerns/cron_schedulable.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module CronSchedulable
+ extend ActiveSupport::Concern
+ include Schedulable
+
+ ##
+ # The `next_run_at` column is set to the actual execution date of worker that
+ # triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
+ # in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
+ def set_next_run_at
+ now = Time.zone.now
+ ideal_next_run = ideal_next_run_from(now)
+
+ self.next_run_at = if ideal_next_run == cron_worker_next_run_from(now)
+ ideal_next_run
+ else
+ cron_worker_next_run_from(ideal_next_run)
+ end
+ end
+
+ private
+
+ def ideal_next_run_from(start_time)
+ next_time_from(start_time, cron, cron_timezone)
+ end
+
+ def cron_worker_next_run_from(start_time)
+ next_time_from(start_time, worker_cron_expression, Time.zone.name)
+ end
+
+ def next_time_from(start_time, cron, cron_timezone)
+ Gitlab::Ci::CronParser
+ .new(cron, cron_timezone)
+ .next_time_from(start_time)
+ end
+
+ def worker_cron_expression
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index de17f50cd29..2e368b12cb7 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -22,6 +22,8 @@ module Enums
forward_deployment_failure: 13,
user_blocked: 14,
project_deleted: 15,
+ ci_quota_exceeded: 16,
+ pipeline_loop_detected: 17,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
@@ -35,4 +37,4 @@ module Enums
end
end
-Enums::Ci::CommitStatus.prepend_if_ee('EE::Enums::Ci::CommitStatus')
+Enums::Ci::CommitStatus.prepend_mod_with('Enums::Ci::CommitStatus')
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index fdc48d09db2..c42b046592f 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -10,6 +10,7 @@ module Enums
unknown_failure: 0,
config_error: 1,
external_validation_failure: 2,
+ user_not_verified: 3,
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb
index b08c05b1934..71c86bab136 100644
--- a/app/models/concerns/enums/internal_id.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -22,4 +22,4 @@ module Enums
end
end
-Enums::InternalId.prepend_if_ee('EE::Enums::InternalId')
+Enums::InternalId.prepend_mod_with('Enums::InternalId')
diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb
index 4b2e9e9e0b2..55360eb92e6 100644
--- a/app/models/concerns/enums/vulnerability.rb
+++ b/app/models/concerns/enums/vulnerability.rb
@@ -43,4 +43,4 @@ module Enums
end
end
-Enums::Vulnerability.prepend_if_ee('EE::Enums::Vulnerability')
+Enums::Vulnerability.prepend_mod_with('Enums::Vulnerability')
diff --git a/app/models/concerns/from_set_operator.rb b/app/models/concerns/from_set_operator.rb
index 593fd251c5c..c6d63631c84 100644
--- a/app/models/concerns/from_set_operator.rb
+++ b/app/models/concerns/from_set_operator.rb
@@ -10,8 +10,8 @@ module FromSetOperator
raise "Trying to redefine method '#{method(method_name)}'" if methods.include?(method_name)
- define_method(method_name) do |members, remove_duplicates: true, alias_as: table_name|
- operator_sql = operator.new(members, remove_duplicates: remove_duplicates).to_sql
+ define_method(method_name) do |members, remove_duplicates: true, remove_order: true, alias_as: table_name|
+ operator_sql = operator.new(members, remove_duplicates: remove_duplicates, remove_order: remove_order).to_sql
from(Arel.sql("(#{operator_sql}) #{alias_as}"))
end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 67953105bed..b376537a418 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -22,7 +22,7 @@ module GroupDescendant
return [] if descendants.empty?
unless descendants.all? { |hierarchy| hierarchy.is_a?(GroupDescendant) }
- raise ArgumentError.new(_('element is not a hierarchy'))
+ raise ArgumentError, _('element is not a hierarchy')
end
all_hierarchies = descendants.map do |descendant|
@@ -56,7 +56,7 @@ module GroupDescendant
end
if parent.nil? && hierarchy_top.present?
- raise ArgumentError.new(_('specified top is not part of the tree'))
+ raise ArgumentError, _('specified top is not part of the tree')
end
if parent && parent != hierarchy_top
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/has_integrations.rb
index 5e53f13be95..b2775f4cbb2 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/has_integrations.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-module Integration
+module HasIntegrations
extend ActiveSupport::Concern
class_methods do
def with_custom_integration_for(integration, page = nil, per = nil)
- custom_integration_project_ids = Service
+ custom_integration_project_ids = Integration
.select(:project_id)
.where(type: integration.type)
.where(inherit_from_id: nil)
@@ -17,13 +17,13 @@ module Integration
end
def without_integration(integration)
- services = Service
+ integrations = Integration
.select('1')
.where('services.project_id = projects.id')
.where(type: integration.type)
Project
- .where('NOT EXISTS (?)', services)
+ .where('NOT EXISTS (?)', integrations)
.where(pending_delete: false)
.where(archived: false)
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 774cda2c3e8..33f6904bc91 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -17,7 +17,7 @@ module HasRepository
def valid_repo?
repository.exists?
- rescue
+ rescue StandardError
errors.add(:base, _('Invalid repository path'))
false
end
@@ -25,7 +25,7 @@ module HasRepository
def repo_exists?
strong_memoize(:repo_exists) do
repository.exists?
- rescue
+ rescue StandardError
false
end
end
diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb
index 90f9876de95..3af063438bf 100644
--- a/app/models/concerns/has_timelogs_report.rb
+++ b/app/models/concerns/has_timelogs_report.rb
@@ -15,6 +15,6 @@ module HasTimelogsReport
private
def timelogs_for(start_time, end_time)
- Timelog.between_times(start_time, end_time).for_issues_in_group(self)
+ Timelog.between_times(start_time, end_time).in_group(self)
end
end
diff --git a/app/models/concerns/has_wiki_page_meta_attributes.rb b/app/models/concerns/has_wiki_page_meta_attributes.rb
index 136f2d00ce3..55681bc91a5 100644
--- a/app/models/concerns/has_wiki_page_meta_attributes.rb
+++ b/app/models/concerns/has_wiki_page_meta_attributes.rb
@@ -59,7 +59,7 @@ module HasWikiPageMetaAttributes
if conflict.present?
meta.errors.add(:canonical_slug, 'Duplicate value found')
- raise CanonicalSlugConflictError.new(meta)
+ raise CanonicalSlugConflictError, meta
end
meta
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 1e44321e148..f5c70f10dc5 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -63,7 +63,7 @@ module Issuable
has_many :note_authors, -> { distinct }, through: :notes, source: :author
- has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
+ has_many :label_links, as: :target, inverse_of: :target
has_many :labels, through: :label_links
has_many :todos, as: :target
@@ -103,7 +103,7 @@ module Issuable
end
scope :assigned_to, ->(u) do
assignees_table = Arel::Table.new("#{to_ability_name}_assignees")
- sql = assignees_table.project('true').where(assignees_table[:user_id].in(u)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ sql = assignees_table.project('true').where(assignees_table[:user_id].in(u.id)).where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
where("EXISTS (#{sql.to_sql})")
end
# rubocop:enable GitlabSecurity/SqlInjection
@@ -564,4 +564,4 @@ module Issuable
end
end
-Issuable.prepend_if_ee('EE::Issuable')
+Issuable.prepend_mod_with('Issuable')
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index a5ffa959174..28d12a033a6 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -29,5 +29,5 @@ module IssueAvailableFeatures
end
end
-IssueAvailableFeatures.prepend_if_ee('EE::IssueAvailableFeatures')
-IssueAvailableFeatures::ClassMethods.prepend_if_ee('EE::IssueAvailableFeatures::ClassMethods')
+IssueAvailableFeatures.prepend_mod_with('IssueAvailableFeatures')
+IssueAvailableFeatures::ClassMethods.prepend_mod_with('IssueAvailableFeatures::ClassMethods')
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 3cb0bd85936..672bcdbbb1b 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -7,6 +7,7 @@ module Limitable
included do
class_attribute :limit_scope
class_attribute :limit_name
+ class_attribute :limit_feature_flag
self.limit_name = self.name.demodulize.tableize
validate :validate_plan_limit_not_exceeded, on: :create
@@ -25,6 +26,7 @@ module Limitable
def validate_scoped_plan_limit_not_exceeded
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
return unless scope_relation
+ return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation, default_enabled: :yaml)
relation = self.class.where(limit_scope => scope_relation)
limits = scope_relation.actual_limits
diff --git a/app/models/concerns/loaded_in_group_list.rb b/app/models/concerns/loaded_in_group_list.rb
index 59e0ed75d2d..848ef63f1c2 100644
--- a/app/models/concerns/loaded_in_group_list.rb
+++ b/app/models/concerns/loaded_in_group_list.rb
@@ -79,4 +79,4 @@ module LoadedInGroupList
end
end
-LoadedInGroupList::ClassMethods.prepend_if_ee('EE::LoadedInGroupList::ClassMethods')
+LoadedInGroupList::ClassMethods.prepend_mod_with('LoadedInGroupList::ClassMethods')
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 5db077c178d..f1baa923ec5 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -211,4 +211,4 @@ module Mentionable
end
end
-Mentionable.prepend_if_ee('EE::Mentionable')
+Mentionable.prepend_mod_with('Mentionable')
diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb
index 5a5ce1809d0..e33b6db0103 100644
--- a/app/models/concerns/mentionable/reference_regexes.rb
+++ b/app/models/concerns/mentionable/reference_regexes.rb
@@ -37,4 +37,4 @@ module Mentionable
end
end
-Mentionable::ReferenceRegexes.prepend_if_ee('EE::Mentionable::ReferenceRegexes')
+Mentionable::ReferenceRegexes.prepend_mod_with('Mentionable::ReferenceRegexes')
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index d42417bb6c1..c4f810ab9b1 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -28,7 +28,7 @@ module Milestoneable
scope :without_release, -> do
joins("LEFT OUTER JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id")
- .where('milestone_releases.release_id IS NULL')
+ .where(milestone_releases: { release_id: nil })
end
scope :joins_milestone_releases, -> do
@@ -57,4 +57,4 @@ module Milestoneable
end
end
-Milestoneable.prepend_if_ee('EE::Milestoneable')
+Milestoneable.prepend_mod_with('Milestoneable')
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f3cc68e4b85..f6d4e5bd27b 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -183,5 +183,5 @@ end
Noteable.extend(Noteable::ClassMethods)
-Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
-Noteable.prepend_if_ee('EE::Noteable')
+Noteable::ClassMethods.prepend_mod_with('Noteable::ClassMethods')
+Noteable.prepend_mod_with('Noteable')
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
index c7af841e450..19d2ac620f3 100644
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -28,7 +28,6 @@ module OptimizedIssuableLabelFilter
# Taken from IssuableFinder
def count_by_state
- return super if root_namespace.nil?
return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
count_params = params.merge(state: nil, sort: nil, force_cte: true)
@@ -40,7 +39,11 @@ module OptimizedIssuableLabelFilter
.group(:state_id)
.count
- counts = state_counts.transform_keys { |key| count_key(key) }
+ counts = Hash.new(0)
+
+ state_counts.each do |key, value|
+ counts[count_key(key)] += value
+ end
counts[:all] = counts.values.sum
counts.with_indifferent_access
diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb
index 760ebb49980..e2fa0ceb0f6 100644
--- a/app/models/concerns/packages/debian/architecture.rb
+++ b/app/models/concerns/packages/debian/architecture.rb
@@ -23,6 +23,7 @@ module Packages
uniqueness: { scope: %i[distribution_id] },
format: { with: Gitlab::Regex.debian_architecture_regex }
+ scope :ordered_by_name, -> { order(:name) }
scope :with_distribution, ->(distribution) { where(distribution: distribution) }
scope :with_name, ->(name) { where(name: name) }
end
diff --git a/app/models/concerns/packages/debian/component.rb b/app/models/concerns/packages/debian/component.rb
index 7b342c7b684..5ea686faec2 100644
--- a/app/models/concerns/packages/debian/component.rb
+++ b/app/models/concerns/packages/debian/component.rb
@@ -23,6 +23,7 @@ module Packages
uniqueness: { scope: %i[distribution_id] },
format: { with: Gitlab::Regex.debian_component_regex }
+ scope :ordered_by_name, -> { order(:name) }
scope :with_distribution, ->(distribution) { where(distribution: distribution) }
scope :with_name, ->(name) { where(name: name) }
end
diff --git a/app/models/concerns/packages/debian/component_file.rb b/app/models/concerns/packages/debian/component_file.rb
index 3cc2c291e96..c41635a0d16 100644
--- a/app/models/concerns/packages/debian/component_file.rb
+++ b/app/models/concerns/packages/debian/component_file.rb
@@ -60,6 +60,8 @@ module Packages
scope :preload_distribution, -> { includes(component: :distribution) }
+ scope :created_before, ->(reference) { where("#{table_name}.created_at < ?", reference) }
+
mount_file_store_uploader Packages::Debian::ComponentFileUploader
before_validation :update_size_from_file
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 08fb9ccf3ea..267c7a4d201 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -84,7 +84,7 @@ module Packages
attr_encrypted :signing_keys,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index acd654bd229..25410a859e9 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -135,4 +135,4 @@ module Participable
end
end
-Participable.prepend_if_ee('EE::Participable')
+Participable.prepend_mod_with('Participable')
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 7c774d8bad7..484c91e0833 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -109,4 +109,4 @@ module ProjectFeaturesCompatibility
end
end
-ProjectFeaturesCompatibility.prepend_if_ee('EE::ProjectFeaturesCompatibility')
+ProjectFeaturesCompatibility.prepend_mod_with('ProjectFeaturesCompatibility')
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index 55c2bf96a94..afebc426762 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -26,9 +26,14 @@ module PrometheusAdapter
}
end
+ # Overridden in app/models/clusters/applications/prometheus.rb
+ def managed_prometheus?
+ false
+ end
+
# This is a light-weight check if a prometheus client is properly configured.
def configured?
- raise NotImplemented
+ raise NotImplementedError
end
# This is a heavy-weight check if a prometheus is properly configured and accessible from GitLab.
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 2828ae4a3a9..ec56f4a32af 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -78,4 +78,4 @@ end
# since these are defined in a ClassMethods constant. As such, we prepend the
# module directly into ProtectedRef::ClassMethods, instead of prepending it into
# ProtectedRef.
-ProtectedRef::ClassMethods.prepend_if_ee('EE::ProtectedRef')
+ProtectedRef::ClassMethods.prepend_mod_with('ProtectedRef')
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 5e38ce7cad8..618ad96905d 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -53,12 +53,12 @@ module ProtectedRefAccess
end
end
-ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
-ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
+ProtectedRefAccess.include_mod_with('ProtectedRefAccess::Scopes')
+ProtectedRefAccess.prepend_mod_with('ProtectedRefAccess')
# When using `prepend` (or `include` for that matter), the `ClassMethods`
# constants are not merged. This means that `class_methods` in
# `EE::ProtectedRefAccess` would be ignored.
#
# To work around this, we prepend the `ClassMethods` constant manually.
-ProtectedRefAccess::ClassMethods.prepend_if_ee('EE::ProtectedRefAccess::ClassMethods')
+ProtectedRefAccess::ClassMethods.prepend_mod_with('ProtectedRefAccess::ClassMethods')
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index dbc70ac2218..9ed2070d11c 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -168,7 +168,7 @@ module ReactiveCaching
data_deep_size = Gitlab::Utils::DeepSize.new(data, max_size: self.class.reactive_cache_hard_limit)
- raise ExceededReactiveCacheLimit.new unless data_deep_size.valid?
+ raise ExceededReactiveCacheLimit unless data_deep_size.valid?
end
end
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 7f559f0a7ed..75dfed6d58f 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -53,13 +53,13 @@ module RelativePositioning
return [size, starting_from] if size >= MIN_GAP
+ terminus = context.at_position(starting_from)
+
if at_end
- terminus = context.max_sibling
terminus.shift_left
max_relative_position = terminus.relative_position
[[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
else
- terminus = context.min_sibling
terminus.shift_right
min_relative_position = terminus.relative_position
[[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
@@ -79,6 +79,8 @@ module RelativePositioning
objects = objects.reject(&:relative_position)
return 0 if objects.empty?
+ objects.first.check_repositioning_allowed!
+
number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
representative = RelativePositioning.mover.context(objects.first)
@@ -123,6 +125,12 @@ module RelativePositioning
::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
end
+ # To be overriden on child classes whenever
+ # blocking position updates is necessary.
+ def check_repositioning_allowed!
+ nil
+ end
+
def move_between(before, after)
before, after = [before, after].sort_by(&:relative_position) if before && after
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 8607f0d94f4..1dd8eebeff3 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -50,7 +50,7 @@ module RepositoryStorageMovable
begin
storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
- rescue => err
+ rescue StandardError => err
storage_move.add_error(err.message)
next false
end
@@ -114,7 +114,7 @@ module RepositoryStorageMovable
private
def container_repository_writable
- add_error(_('is read only')) if container&.repository_read_only?
+ add_error(_('is read-only')) if container&.repository_read_only?
end
def error_key
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 71d8e06de76..847abdc1b6d 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -96,11 +96,49 @@ module Routable
end
def full_name
- route&.name || build_full_name
+ # We have to test for persistence as the cache key uses #updated_at
+ return (route&.name || build_full_name) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+
+ # Return the name as-is if the parent is missing
+ return name if route.nil? && parent.nil? && name.present?
+
+ # If the route is already preloaded, return directly, preventing an extra load
+ return route.name if route_loaded? && route.present?
+
+ # Similarly, we can allow the build if the parent is loaded
+ return build_full_name if parent_loaded?
+
+ Gitlab::Cache.fetch_once([cache_key, :full_name]) do
+ route&.name || build_full_name
+ end
end
def full_path
- route&.path || build_full_path
+ # We have to test for persistence as the cache key uses #updated_at
+ return (route&.path || build_full_path) unless persisted? && Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml)
+
+ # Return the path as-is if the parent is missing
+ return path if route.nil? && parent.nil? && path.present?
+
+ # If the route is already preloaded, return directly, preventing an extra load
+ return route.path if route_loaded? && route.present?
+
+ # Similarly, we can allow the build if the parent is loaded
+ return build_full_path if parent_loaded?
+
+ Gitlab::Cache.fetch_once([cache_key, :full_path]) do
+ route&.path || build_full_path
+ end
+ end
+
+ # Overriden in the Project model
+ # parent_id condition prevents issues with parent reassignment
+ def parent_loaded?
+ association(:parent).loaded?
+ end
+
+ def route_loaded?
+ association(:route).loaded?
end
def full_path_components
@@ -124,7 +162,9 @@ module Routable
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
- self.errors[:path].concat(route_path_errors) if route_path_errors
+ route_path_errors&.each do |msg|
+ self.errors.add(:path, msg)
+ end
end
def full_name_changed?
diff --git a/app/models/concerns/services/data_fields.rb b/app/models/concerns/services/data_fields.rb
index 10963e4e7d8..fd56af449bc 100644
--- a/app/models/concerns/services/data_fields.rb
+++ b/app/models/concerns/services/data_fields.rb
@@ -5,11 +5,11 @@ module Services
extend ActiveSupport::Concern
included do
- belongs_to :service
+ belongs_to :integration, inverse_of: self.name.underscore.to_sym, foreign_key: :service_id
- delegate :activated?, to: :service, allow_nil: true
+ delegate :activated?, to: :integration, allow_nil: true
- validates :service, presence: true
+ validates :integration, presence: true
end
class_methods do
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 9dfe1b77829..4921f7f1a7e 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -31,9 +31,9 @@ module Sha256Attribute
end
unless column.type == :binary
- raise ArgumentError.new("sha256_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "sha256_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "Sha256Attribute initialization: #{error.message}"
raise
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index cbac6a210c7..f6f5dbce4b6 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -24,9 +24,9 @@ module ShaAttribute
return unless column
unless column.type == :binary
- raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
raise
end
@@ -37,4 +37,4 @@ module ShaAttribute
end
end
-ShaAttribute::ClassMethods.prepend_if_ee('EE::ShaAttribute')
+ShaAttribute::ClassMethods.prepend_mod_with('ShaAttribute')
diff --git a/app/models/concerns/sidebars/container_with_html_options.rb b/app/models/concerns/sidebars/container_with_html_options.rb
deleted file mode 100644
index 12ea366c66a..00000000000
--- a/app/models/concerns/sidebars/container_with_html_options.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module ContainerWithHtmlOptions
- # The attributes returned from this method
- # will be applied to helper methods like
- # `link_to` or the div containing the container.
- def container_html_options
- {
- aria: { label: title }
- }.merge(extra_container_html_options)
- end
-
- # Classes will override mostly this method
- # and not `container_html_options`.
- def extra_container_html_options
- {}
- end
-
- # Attributes to pass to the html_options attribute
- # in the helper method that sets the active class
- # on each element.
- def nav_link_html_options
- {}
- end
-
- def title
- raise NotImplementedError
- end
-
- # The attributes returned from this method
- # will be applied right next to the title,
- # for example in the span that renders the title.
- def title_html_options
- {}
- end
-
- def link
- raise NotImplementedError
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_active_routes.rb b/app/models/concerns/sidebars/has_active_routes.rb
deleted file mode 100644
index e7a153f067a..00000000000
--- a/app/models/concerns/sidebars/has_active_routes.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module HasActiveRoutes
- # This method will indicate for which paths or
- # controllers, the menu or menu item should
- # be set as active.
- #
- # The returned values are passed to the `nav_link` helper method,
- # so the params can be either `path`, `page`, `controller`.
- # Param 'action' is not supported.
- def active_routes
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_hint.rb b/app/models/concerns/sidebars/has_hint.rb
deleted file mode 100644
index 21dca39dca0..00000000000
--- a/app/models/concerns/sidebars/has_hint.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# This module has the necessary methods to store
-# hints for menus. Hints are elements displayed
-# when the user hover the menu item.
-module Sidebars
- module HasHint
- def show_hint?
- false
- end
-
- def hint_html_options
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_icon.rb b/app/models/concerns/sidebars/has_icon.rb
deleted file mode 100644
index d1a87918285..00000000000
--- a/app/models/concerns/sidebars/has_icon.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# This module has the necessary methods to show
-# sprites or images next to the menu item.
-module Sidebars
- module HasIcon
- def sprite_icon
- nil
- end
-
- def sprite_icon_html_options
- {}
- end
-
- def image_path
- nil
- end
-
- def image_html_options
- {}
- end
-
- def icon_or_image?
- sprite_icon || image_path
- end
- end
-end
diff --git a/app/models/concerns/sidebars/has_pill.rb b/app/models/concerns/sidebars/has_pill.rb
deleted file mode 100644
index ad7064fe63d..00000000000
--- a/app/models/concerns/sidebars/has_pill.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# This module introduces the logic to show the "pill" element
-# next to the menu item, indicating the a count.
-module Sidebars
- module HasPill
- def has_pill?
- false
- end
-
- # In this method we will need to provide the query
- # to retrieve the elements count
- def pill_count
- raise NotImplementedError
- end
-
- def pill_html_options
- {}
- end
- end
-end
diff --git a/app/models/concerns/sidebars/positionable_list.rb b/app/models/concerns/sidebars/positionable_list.rb
deleted file mode 100644
index 30830d547f3..00000000000
--- a/app/models/concerns/sidebars/positionable_list.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-# This module handles elements in a list. All elements
-# must have a different class
-module Sidebars
- module PositionableList
- def add_element(list, element)
- list << element
- end
-
- def insert_element_before(list, before_element, new_element)
- index = index_of(list, before_element)
-
- if index
- list.insert(index, new_element)
- else
- list.unshift(new_element)
- end
- end
-
- def insert_element_after(list, after_element, new_element)
- index = index_of(list, after_element)
-
- if index
- list.insert(index + 1, new_element)
- else
- add_element(list, new_element)
- end
- end
-
- private
-
- def index_of(list, element)
- list.index { |e| e.is_a?(element) }
- end
- end
-end
diff --git a/app/models/concerns/sidebars/renderable.rb b/app/models/concerns/sidebars/renderable.rb
deleted file mode 100644
index a3976af8515..00000000000
--- a/app/models/concerns/sidebars/renderable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Renderable
- # This method will control whether the menu or menu_item
- # should be rendered. It will be overriden by specific
- # classes.
- def render?
- true
- end
- end
-end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index a82cf338039..948190dfadf 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -10,7 +10,7 @@ module Storage
proj_with_tags = first_project_with_container_registry_tags
if proj_with_tags
- raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry")
+ raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
end
parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
@@ -48,7 +48,7 @@ module Storage
begin
send_update_instructions
write_projects_repository_config
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
full_path_before_last_save: full_path_before_last_save,
full_path: full_path,
@@ -83,7 +83,7 @@ module Storage
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
end
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index d8867177059..4d1c1d44af7 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -12,7 +12,7 @@ module Taskable
COMPLETED = 'completed'
INCOMPLETE = 'incomplete'
COMPLETE_PATTERN = /(\[[xX]\])/.freeze
- INCOMPLETE_PATTERN = /(\[[\s]\])/.freeze
+ INCOMPLETE_PATTERN = /(\[\s\])/.freeze
ITEM_PATTERN = %r{
^
(?:(?:>\s{0,4})*) # optional blockquote characters
diff --git a/app/models/concerns/throttled_touch.rb b/app/models/concerns/throttled_touch.rb
index 797c46f6cc5..b5682abb229 100644
--- a/app/models/concerns/throttled_touch.rb
+++ b/app/models/concerns/throttled_touch.rb
@@ -6,7 +6,7 @@ module ThrottledTouch
# The amount of time to wait before "touch" can update a record again.
TOUCH_INTERVAL = 1.minute
- def touch(*args)
+ def touch(*args, **kwargs)
super if (Time.zone.now - updated_at) > TOUCH_INTERVAL
end
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 8273059b30c..fb9a8cd312d 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -72,11 +72,7 @@ module Timebox
groups = groups.compact if groups.is_a? Array
groups = [] if groups.nil?
- if Feature.enabled?(:optimized_timebox_queries, default_enabled: true)
- from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
- else
- where(project_id: projects).or(where(group_id: groups))
- end
+ from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
end
# A timebox is within the timeframe (start_date, end_date) if it overlaps
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 535cf25eb9d..34c8630bb90 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -12,7 +12,7 @@ module TokenAuthenticatable
def add_authentication_token_field(token_field, options = {})
if token_authenticatable_fields.include?(token_field)
- raise ArgumentError.new("#{token_field} already configured via add_authentication_token_field")
+ raise ArgumentError, "#{token_field} already configured via add_authentication_token_field"
end
token_authenticatable_fields.push(token_field)
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index db5df6c2c9f..8fe34632430 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -29,11 +29,11 @@ module TriggerableHooks
callable_scopes = triggers.keys + [:all]
return none unless callable_scopes.include?(trigger)
- public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
+ executable.public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend
end
def select_active(hooks_scope, data)
- select do |hook|
+ executable.select do |hook|
ActiveHookFilter.new(hook).matches?(hooks_scope, data)
end
end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index cf50305faab..f0e5e010e70 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -4,4 +4,4 @@ module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
end
-VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
+VulnerabilityFindingHelpers.prepend_mod_with('VulnerabilityFindingHelpers')
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
index f57e3cb0bfb..f98c1e93aaf 100644
--- a/app/models/concerns/vulnerability_finding_signature_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -4,4 +4,4 @@ module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
end
-VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
+VulnerabilityFindingSignatureHelpers.prepend_mod_with('VulnerabilityFindingSignatureHelpers')
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index d2a5c736604..dbba80eff53 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -31,9 +31,9 @@ module X509SerialNumberAttribute
end
unless column.type == :binary
- raise ArgumentError.new("x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary")
+ raise ArgumentError, "x509_serial_number_attribute #{name.inspect} is invalid since the column type is not :binary"
end
- rescue => error
+ rescue StandardError => error
Gitlab::AppLogger.error "X509SerialNumberAttribute initialization: #{error.message}"
raise
end
diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb
index 109fda675a2..c1b865ae578 100644
--- a/app/models/container_registry/event.rb
+++ b/app/models/container_registry/event.rb
@@ -66,4 +66,4 @@ module ContainerRegistry
end
end
-::ContainerRegistry::Event.prepend_if_ee('EE::ContainerRegistry::Event')
+::ContainerRegistry::Event.prepend_mod_with('ContainerRegistry::Event')
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index e2bdf8ffce2..6e0d0e347c9 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -7,6 +7,7 @@ class ContainerRepository < ApplicationRecord
include Sortable
WAITING_CLEANUP_STATUSES = %i[cleanup_scheduled cleanup_unfinished].freeze
+ REQUIRING_CLEANUP_STATUSES = %i[cleanup_unscheduled cleanup_scheduled].freeze
belongs_to :project
@@ -31,6 +32,7 @@ class ContainerRepository < ApplicationRecord
scope :for_project_id, ->(project_id) { where(project_id: project_id) }
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
scope :waiting_for_cleanup, -> { where(expiration_policy_cleanup_status: WAITING_CLEANUP_STATUSES) }
+ scope :expiration_policy_started_at_nil_or_before, ->(timestamp) { where('expiration_policy_started_at < ? OR expiration_policy_started_at IS NULL', timestamp) }
def self.exists_by_path?(path)
where(
@@ -39,6 +41,23 @@ class ContainerRepository < ApplicationRecord
).exists?
end
+ def self.with_enabled_policy
+ joins("INNER JOIN container_expiration_policies ON container_repositories.project_id = container_expiration_policies.project_id")
+ .where(container_expiration_policies: { enabled: true })
+ end
+
+ def self.requiring_cleanup
+ where(
+ container_repositories: { expiration_policy_cleanup_status: REQUIRING_CLEANUP_STATUSES },
+ project_id: ::ContainerExpirationPolicy.runnable_schedules
+ .select(:project_id)
+ )
+ end
+
+ def self.with_unfinished_cleanup
+ with_enabled_policy.cleanup_unfinished
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def registry
@registry ||= begin
@@ -140,4 +159,4 @@ class ContainerRepository < ApplicationRecord
end
end
-ContainerRepository.prepend_if_ee('EE::ContainerRepository')
+ContainerRepository.prepend_mod_with('ContainerRepository')
diff --git a/app/models/context_commits_diff.rb b/app/models/context_commits_diff.rb
new file mode 100644
index 00000000000..fe1a72b79f2
--- /dev/null
+++ b/app/models/context_commits_diff.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+class ContextCommitsDiff
+ include ActsAsPaginatedDiff
+
+ attr_reader :merge_request
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ end
+
+ def empty?
+ commits.empty?
+ end
+
+ def commits_count
+ merge_request.context_commits_count
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Compare.new(
+ self,
+ project: merge_request.project,
+ diff_options: diff_options,
+ diff_refs: diff_refs
+ )
+ end
+
+ def raw_diffs(options = {})
+ compare.diffs(options.merge(paths: paths))
+ end
+
+ def diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: commits.last&.diff_refs&.base_sha,
+ head_sha: commits.first&.diff_refs&.head_sha
+ )
+ end
+
+ private
+
+ def compare
+ @compare ||=
+ Gitlab::Git::Compare.new(
+ merge_request.project.repository.raw_repository,
+ commits.last&.diff_refs&.base_sha,
+ commits.first&.diff_refs&.head_sha
+ )
+ end
+
+ def commits
+ @commits ||= merge_request.project.repository.commits_by(oids: merge_request.recent_context_commits.map(&:id))
+ end
+
+ def paths
+ merge_request.merge_request_context_commit_diff_files.map(&:path)
+ end
+end
diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb
index dd4afa9b809..5538e93a39e 100644
--- a/app/models/cycle_analytics/project_level_stage_adapter.rb
+++ b/app/models/cycle_analytics/project_level_stage_adapter.rb
@@ -4,6 +4,8 @@
# compatible with the old value stream controller actions.
module CycleAnalytics
class ProjectLevelStageAdapter
+ ProjectLevelStage = Struct.new(:title, :description, :legend, :name, :project_median, keyword_init: true )
+
def initialize(stage, options)
@stage = stage
@options = options
@@ -13,7 +15,7 @@ module CycleAnalytics
def as_json(serializer: AnalyticsStageSerializer)
presenter = Analytics::CycleAnalytics::StagePresenter.new(stage)
- serializer.new.represent(OpenStruct.new(
+ serializer.new.represent(ProjectLevelStage.new(
title: presenter.title,
description: presenter.description,
legend: presenter.legend,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index d3280403bfd..e2b25690323 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -32,8 +32,9 @@ class Deployment < ApplicationRecord
delegate :kubernetes_namespace, to: :deployment_cluster, allow_nil: true
scope :for_environment, -> (environment) { where(environment_id: environment) }
- scope :for_environment_name, -> (name) do
- joins(:environment).where(environments: { name: name })
+ scope :for_environment_name, -> (project, name) do
+ where('deployments.environment_id = (?)',
+ Environment.select(:id).where(project: project, name: name).limit(1))
end
scope :for_status, -> (status) { where(status: status) }
@@ -87,7 +88,7 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
deployment.run_after_commit do
- Deployments::ExecuteHooksWorker.perform_async(id)
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end
@@ -100,7 +101,7 @@ class Deployment < ApplicationRecord
after_transition any => FINISHED_STATUSES do |deployment|
deployment.run_after_commit do
- Deployments::ExecuteHooksWorker.perform_async(id)
+ Deployments::HooksWorker.perform_async(deployment_id: id, status_changed_at: Time.current)
end
end
@@ -182,8 +183,8 @@ class Deployment < ApplicationRecord
Commit.truncate_sha(sha)
end
- def execute_hooks
- deployment_data = Gitlab::DataBuilder::Deployment.build(self)
+ def execute_hooks(status_changed_at)
+ deployment_data = Gitlab::DataBuilder::Deployment.build(self, status_changed_at)
project.execute_hooks(deployment_data, :deployment_hooks)
project.execute_services(deployment_data, :deployment_hooks)
end
@@ -347,4 +348,4 @@ class Deployment < ApplicationRecord
end
end
-Deployment.prepend_if_ee('EE::Deployment')
+Deployment.prepend_mod_with('Deployment')
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
index 7949bd81605..b91785eeb57 100644
--- a/app/models/deployment_merge_request.rb
+++ b/app/models/deployment_merge_request.rb
@@ -12,7 +12,7 @@ class DeploymentMergeRequest < ApplicationRecord
end
def self.by_deployment_id(id)
- where('deployments.id = ?', id)
+ where(deployments: { id: id })
end
def self.deployed_to(name)
@@ -20,7 +20,7 @@ class DeploymentMergeRequest < ApplicationRecord
# (project_id, name), instead of using the index on
# (name varchar_pattern_ops). This results in better performance on
# GitLab.com.
- where('environments.name = ?', name)
+ where(environments: { name: name })
.where('environments.project_id = merge_requests.target_project_id')
end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index f69564f4893..96c8553c101 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -29,4 +29,4 @@ class DescriptionVersion < ApplicationRecord
end
end
-DescriptionVersion.prepend_if_ee('EE::DescriptionVersion')
+DescriptionVersion.prepend_mod_with('DescriptionVersion')
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index 5cfd8f3ec8e..ca65cf38f0d 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -58,6 +58,7 @@ module DesignManagement
scope :ordered, -> { order(id: :desc) }
scope :for_issue, -> (issue) { where(issue: issue) }
scope :by_sha, -> (sha) { where(sha: sha) }
+ scope :with_author, -> { includes(:author) }
# This is the one true way to create a Version.
#
@@ -94,7 +95,7 @@ module DesignManagement
version
end
- rescue
+ rescue StandardError
raise CouldNotCreateVersion.new(sha, issue_id, design_actions)
end
diff --git a/app/models/discussion_note.rb b/app/models/discussion_note.rb
index 5049107da2c..6621b30b645 100644
--- a/app/models/discussion_note.rb
+++ b/app/models/discussion_note.rb
@@ -5,7 +5,7 @@
# A note of this type can be resolvable.
class DiscussionNote < Note
# This prepend must stay here because the `validates` below depends on it.
- prepend_if_ee('EE::DiscussionNote') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('DiscussionNote') # rubocop: disable Cop/InjectEnterpriseEditionModule
# Names of all implementers of `Noteable` that support discussions.
def self.noteable_types
diff --git a/app/models/email.rb b/app/models/email.rb
index c5154267ff0..0140f784842 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -22,7 +22,7 @@ class Email < ApplicationRecord
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
- delegate :username, :can?, to: :user
+ delegate :username, :can?, :pending_invitations, :accept_pending_invitations!, to: :user
def email=(value)
write_attribute(:email, value.downcase.strip)
@@ -32,10 +32,6 @@ class Email < ApplicationRecord
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
- def accept_pending_invitations!
- user.accept_pending_invitations!
- end
-
def validate_email_format
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 4ee93b0ba4a..2e677a3d177 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -24,13 +24,13 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
- has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
+ has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
- has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
+ has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
@@ -269,7 +269,7 @@ class Environment < ApplicationRecord
Gitlab::OptimisticLocking.retry_lock(deployment.deployable, name: 'environment_cancel_deployment_jobs') do |deployable|
deployable.cancel! if deployable&.cancelable?
end
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, environment_id: id, deployment_id: deployment.id)
end
end
@@ -406,7 +406,7 @@ class Environment < ApplicationRecord
end
def elastic_stack_available?
- !!deployment_platform&.cluster&.application_elastic_stack_available?
+ !!deployment_platform&.cluster&.elastic_stack_available?
end
def rollout_status
@@ -471,4 +471,4 @@ class Environment < ApplicationRecord
end
end
-Environment.prepend_if_ee('EE::Environment')
+Environment.prepend_mod_with('Environment')
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 93f286f97d3..81cd342576f 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -18,4 +18,4 @@ class Epic < ApplicationRecord
end
end
-Epic.prepend_if_ee('EE::Epic')
+Epic.prepend_mod_with('Epic')
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 9a9fbc6a801..956b5d6470f 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -38,7 +38,7 @@ module ErrorTracking
attr_encrypted :token,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
after_save :clear_reactive_cache!
diff --git a/app/models/event.rb b/app/models/event.rb
index 401dfc4cb02..5b755736f47 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -448,4 +448,4 @@ class Event < ApplicationRecord
end
end
-Event.prepend_if_ee('EE::Event')
+Event.prepend_mod_with('Event')
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
index 1487a6387f0..3fc166203e7 100644
--- a/app/models/external_pull_request.rb
+++ b/app/models/external_pull_request.rb
@@ -72,6 +72,10 @@ class ExternalPullRequest < ApplicationRecord
end
end
+ def modified_paths
+ project.repository.diff_stats(target_sha, source_sha).paths
+ end
+
private
def actual_source_branch_sha
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 330815ab8c1..0cb3662368c 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -128,4 +128,4 @@ class GpgKey < ApplicationRecord
end
end
-GpgKey.prepend_if_ee('EE::GpgKey')
+GpgKey.prepend_mod_with('GpgKey')
diff --git a/app/models/group.rb b/app/models/group.rb
index 2967c1ffc1d..da795651c63 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -34,7 +34,7 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
- has_many :services
+ has_many :integrations
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
@@ -67,6 +67,8 @@ class Group < Namespace
has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
+ has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group
+
has_many :group_deploy_keys_groups, inverse_of: :group
has_many :group_deploy_keys, through: :group_deploy_keys_groups
has_many :group_deploy_tokens
@@ -105,21 +107,21 @@ class Group < Namespace
scope :with_users, -> { includes(:users) }
+ scope :with_onboarding_progress, -> { joins(:onboarding_progress) }
+
scope :by_id, ->(groups) { where(id: groups) }
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
- .where("members.user_id IN (?)", user_ids)
+ .where(members: { user_id: user_ids })
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, -> (user_ids) do
joins(projects: :project_authorizations)
- .where("project_authorizations.user_id IN (?)", user_ids)
+ .where(project_authorizations: { user_id: user_ids })
end
- delegate :default_branch_name, to: :namespace_settings
-
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
@@ -155,7 +157,7 @@ class Group < Namespace
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
- .where('project_namespace.share_with_group_lock = ?', false)
+ .where(project_namespace: { share_with_group_lock: false })
.select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
@@ -163,12 +165,12 @@ class Group < Namespace
end
def without_integration(integration)
- services = Service
+ integrations = Integration
.select('1')
.where('services.group_id = namespaces.id')
.where(type: integration.type)
- where('NOT EXISTS (?)', services)
+ where('NOT EXISTS (?)', integrations)
end
# This method can be used only if all groups have the same top-level
@@ -448,6 +450,20 @@ class Group < Namespace
.where(source_id: id)
end
+ def authorizable_members_with_parents
+ source_ids =
+ if has_parent?
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ group_hierarchy_members = GroupMember.where(source_id: source_ids)
+
+ GroupMember.from_union([group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares]).authorizable
+ end
+
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
@@ -553,11 +569,22 @@ class Group < Namespace
def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.can_admin_all_resources? && !only_concrete_membership
+ # Use the preloaded value that exists instead of performing the db query again(cached or not).
+ # Groups::GroupMembersController#preload_max_access makes use of this by
+ # calling Group#max_member_access. This helps when we have a process
+ # that may query this multiple times from the outside through a policy query
+ # like the GroupPolicy#lookup_access_level! does as a condition for any role
+ return user.max_access_for_group[id] if user.max_access_for_group[id]
+
+ max_member_access(user)
+ end
- max_member_access = members_with_parents.where(user_id: user)
- .reorder(access_level: :desc)
- .first
- &.access_level
+ def max_member_access(user)
+ max_member_access = members_with_parents
+ .where(user_id: user)
+ .reorder(access_level: :desc)
+ .first
+ &.access_level
max_member_access || GroupMember::NO_ACCESS
end
@@ -622,7 +649,7 @@ class Group < Namespace
end
def access_request_approvers_to_be_notified
- members.owners.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.owners.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def supports_events?
@@ -693,6 +720,14 @@ class Group < Namespace
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
+ def to_ability_name
+ model_name.singular
+ end
+
+ def activity_path
+ Gitlab::Routing.url_helpers.activity_group_path(self)
+ end
+
private
def update_two_factor_requirement
@@ -820,7 +855,12 @@ class Group < Namespace
end
def uncached_ci_variables_for(ref, project, environment: nil)
- list_of_ids = [self] + ancestors
+ list_of_ids = if root_ancestor.use_traversal_ids?
+ [self] + ancestors(hierarchy_order: :asc)
+ else
+ [self] + ancestors
+ end
+
variables = Ci::GroupVariable.where(group: list_of_ids)
variables = variables.unprotected unless project.protected_for?(ref)
@@ -835,4 +875,4 @@ class Group < Namespace
end
end
-Group.prepend_if_ee('EE::Group')
+Group.prepend_mod_with('Group')
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b625a70b444..a28b97e63e5 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -4,6 +4,7 @@ class ProjectHook < WebHook
include TriggerableHooks
include Presentable
include Limitable
+ extend ::Gitlab::Utils::Override
self.limit_scope = :project
@@ -29,6 +30,15 @@ class ProjectHook < WebHook
def pluralized_name
_('Webhooks')
end
+
+ def web_hooks_disable_failed?
+ Feature.enabled?(:web_hooks_disable_failed, project)
+ end
+
+ override :rate_limit
+ def rate_limit
+ project.actual_limits.limit_for(:web_hook_calls)
+ end
end
-ProjectHook.prepend_if_ee('EE::ProjectHook')
+ProjectHook.prepend_mod_with('ProjectHook')
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 4caa45a13d4..1a466b333a5 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -3,12 +3,10 @@
class ServiceHook < WebHook
include Presentable
- belongs_to :service
- validates :service, presence: true
+ belongs_to :integration, foreign_key: :service_id
+ validates :integration, presence: true
- # rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name = 'service_hook')
- WebHookService.new(self, data, hook_name).execute
+ super(data, hook_name)
end
- # rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index dbd5a1b032a..02b4feb4ccc 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,6 +3,11 @@
class WebHook < ApplicationRecord
include Sortable
+ FAILURE_THRESHOLD = 3 # three strikes
+ INITIAL_BACKOFF = 10.minutes
+ MAX_BACKOFF = 1.day
+ BACKOFF_GROWTH_FACTOR = 2.0
+
attr_encrypted :token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -21,15 +26,27 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
+ scope :executable, -> do
+ next all unless Feature.enabled?(:web_hooks_disable_failed)
+
+ where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
+ end
+
+ def executable?
+ return true unless web_hooks_disable_failed?
+
+ recent_failures <= FAILURE_THRESHOLD && (disabled_until.nil? || disabled_until < Time.current)
+ end
+
# rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name)
- WebHookService.new(self, data, hook_name).execute
+ WebHookService.new(self, data, hook_name).execute if executable?
end
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
def async_execute(data, hook_name)
- WebHookService.new(self, data, hook_name).async_execute
+ WebHookService.new(self, data, hook_name).async_execute if executable?
end
# rubocop: enable CodeReuse/ServiceClass
@@ -41,4 +58,31 @@ class WebHook < ApplicationRecord
def help_path
'user/project/integrations/webhooks'
end
+
+ def next_backoff
+ return MAX_BACKOFF if backoff_count >= 8 # optimization to prevent expensive exponentiation and possible overflows
+
+ (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**backoff_count))
+ .clamp(INITIAL_BACKOFF, MAX_BACKOFF)
+ .seconds
+ end
+
+ def disable!
+ update!(recent_failures: FAILURE_THRESHOLD + 1)
+ end
+
+ def enable!
+ update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ end
+
+ # Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
+ def rate_limit
+ nil
+ end
+
+ private
+
+ def web_hooks_disable_failed?
+ Feature.enabled?(:web_hooks_disable_failed)
+ end
end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index e2230a2d644..0c96d5d4b6d 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -5,9 +5,12 @@ class WebHookLog < ApplicationRecord
include Presentable
include DeleteWithLimit
include CreatedAtFilterable
+ include PartitionedTable
self.primary_key = :id
+ partitioned_by :created_at, strategy: :monthly
+
belongs_to :web_hook
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
diff --git a/app/models/hooks/web_hook_log_archived.rb b/app/models/hooks/web_hook_log_archived.rb
new file mode 100644
index 00000000000..a1c8a44f5ba
--- /dev/null
+++ b/app/models/hooks/web_hook_log_archived.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# This model is not intended to be used.
+# It is a temporary reference to the old non-partitioned
+# web_hook_logs table.
+# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558
+# for details.
+# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace
+# WebHook, WebHookLog and all hooks are defined outside of a namespace
+class WebHookLogArchived < ApplicationRecord
+ self.table_name = 'web_hook_logs_archived'
+end
diff --git a/app/models/hooks/web_hook_log_partitioned.rb b/app/models/hooks/web_hook_log_partitioned.rb
deleted file mode 100644
index b4b150afb6a..00000000000
--- a/app/models/hooks/web_hook_log_partitioned.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-# This model is not yet intended to be used.
-# It is in a transitioning phase while we are partitioning
-# the web_hook_logs table on the database-side.
-# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/5558
-# for details.
-# rubocop:disable Gitlab/NamespacedClass: This is a temporary class with no relevant namespace
-# WebHook, WebHookLog and all hooks are defined outside of a namespace
-class WebHookLogPartitioned < ApplicationRecord
- include PartitionedTable
-
- self.table_name = 'web_hook_logs_part_0c5294f417'
- self.primary_key = :id
-
- partitioned_by :created_at, strategy: :monthly
-end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index fc97c68b756..df1185f330d 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -51,4 +51,4 @@ class Identity < ApplicationRecord
end
end
-Identity.prepend_if_ee('EE::Identity')
+Identity.prepend_mod_with('Identity')
diff --git a/app/models/identity/uniqueness_scopes.rb b/app/models/identity/uniqueness_scopes.rb
index c1890865a1c..b41b4572e82 100644
--- a/app/models/identity/uniqueness_scopes.rb
+++ b/app/models/identity/uniqueness_scopes.rb
@@ -10,4 +10,4 @@ class Identity < ApplicationRecord
end
end
-Identity::UniquenessScopes.prepend_if_ee('EE::Identity::UniquenessScopes')
+Identity::UniquenessScopes.prepend_mod_with('Identity::UniquenessScopes')
diff --git a/app/models/incident_management/project_incident_management_setting.rb b/app/models/incident_management/project_incident_management_setting.rb
index 4887265be88..b6da93508c2 100644
--- a/app/models/incident_management/project_incident_management_setting.rb
+++ b/app/models/incident_management/project_incident_management_setting.rb
@@ -12,7 +12,7 @@ module IncidentManagement
attr_encrypted :pagerduty_token,
mode: :per_attribute_iv,
- key: ::Settings.attr_encrypted_db_key_base_truncated,
+ key: ::Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false, # No need to encode for binary column https://github.com/attr-encrypted/attr_encrypted#the-encode-encode_iv-encode_salt-and-default_encoding-options
encode_iv: false
@@ -52,4 +52,4 @@ module IncidentManagement
end
end
-IncidentManagement::ProjectIncidentManagementSetting.prepend_if_ee('EE::IncidentManagement::ProjectIncidentManagementSetting')
+IncidentManagement::ProjectIncidentManagementSetting.prepend_mod_with('IncidentManagement::ProjectIncidentManagementSetting')
diff --git a/app/models/instance_metadata.rb b/app/models/instance_metadata.rb
index 96622d0b1b3..6cac78178e0 100644
--- a/app/models/instance_metadata.rb
+++ b/app/models/instance_metadata.rb
@@ -1,10 +1,11 @@
# frozen_string_literal: true
class InstanceMetadata
- attr_reader :version, :revision
+ attr_reader :version, :revision, :kas
def initialize(version: Gitlab::VERSION, revision: Gitlab.revision)
@version = version
@revision = revision
+ @kas = ::InstanceMetadata::Kas.new
end
end
diff --git a/app/models/instance_metadata/kas.rb b/app/models/instance_metadata/kas.rb
new file mode 100644
index 00000000000..7d2d71120b5
--- /dev/null
+++ b/app/models/instance_metadata/kas.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class InstanceMetadata::Kas
+ attr_reader :enabled, :version, :external_url
+
+ def initialize
+ @enabled = Gitlab::Kas.enabled?
+ @version = Gitlab::Kas.version if @enabled
+ @external_url = Gitlab::Kas.external_url if @enabled
+ end
+
+ def self.declarative_policy_class
+ "InstanceMetadataPolicy"
+ end
+end
diff --git a/app/models/service.rb b/app/models/integration.rb
index aadc75ae710..13203cd4e95 100644
--- a/app/models/service.rb
+++ b/app/models/integration.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-# To add new service you should build a class inherited from Service
+# To add new integration you should build a class inherited from Integration
# and implement a set of methods
-class Service < ApplicationRecord
+class Integration < ApplicationRecord
include Sortable
include Importable
include ProjectServicesLoggable
@@ -10,24 +10,29 @@ class Service < ApplicationRecord
include FromUnion
include EachBatch
- SERVICE_NAMES = %w[
+ # TODO Rename the table: https://gitlab.com/gitlab-org/gitlab/-/issues/201856
+ self.table_name = 'services'
+
+ INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
- PROJECT_SPECIFIC_SERVICE_NAMES = %w[
+ PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
datadog jenkins
].freeze
- # Fake services to help with local development.
- DEV_SERVICE_NAMES = %w[
+ # Fake integrations to help with local development.
+ DEV_INTEGRATION_NAMES = %w[
mock_ci mock_monitoring
].freeze
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :type, Gitlab::Integrations::StiType.new
+
default_value_for :active, false
default_value_for :alert_events, true
default_value_for :category, 'common'
@@ -47,18 +52,18 @@ class Service < ApplicationRecord
after_commit :reset_updated_properties
- belongs_to :project, inverse_of: :services
- belongs_to :group, inverse_of: :services
- has_one :service_hook
+ belongs_to :project, inverse_of: :integrations
+ belongs_to :group, inverse_of: :integrations
+ has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
- validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
- validates :group_id, presence: true, unless: -> { template? || instance? || project_id }
- validates :project_id, :group_id, absence: true, if: -> { template? || instance? }
+ validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? }
+ validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? }
+ validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? }
validates :type, presence: true
validates :type, uniqueness: { scope: :template }, if: :template?
- validates :type, uniqueness: { scope: :instance }, if: :instance?
- validates :type, uniqueness: { scope: :project_id }, if: :project_id?
- validates :type, uniqueness: { scope: :group_id }, if: :group_id?
+ validates :type, uniqueness: { scope: :instance }, if: :instance_level?
+ validates :type, uniqueness: { scope: :project_id }, if: :project_level?
+ validates :type, uniqueness: { scope: :group_id }, if: :group_level?
validate :validate_is_instance_or_template
validate :validate_belongs_to_project_or_group
@@ -164,22 +169,23 @@ class Service < ApplicationRecord
end
def self.create_nonexistent_templates
- nonexistent_services = list_nonexistent_services_for(for_template)
+ nonexistent_services = build_nonexistent_services_for(for_template)
return if nonexistent_services.empty?
# Create within a transaction to perform the lowest possible SQL queries.
transaction do
- nonexistent_services.each do |service_type|
- service_type.constantize.create(template: true)
+ nonexistent_services.each do |service|
+ service.template = true
+ service.save
end
end
end
private_class_method :create_nonexistent_templates
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
- if name.in?(available_services_names(include_project_specific: false))
- "#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id)
- end
+ return unless name.in?(available_services_names(include_project_specific: false))
+
+ service_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope)
@@ -187,19 +193,23 @@ class Service < ApplicationRecord
end
def self.build_nonexistent_services_for(scope)
- list_nonexistent_services_for(scope).map do |service_type|
- service_type.constantize.new
+ nonexistent_services_types_for(scope).map do |service_type|
+ service_type_to_model(service_type).new
end
end
private_class_method :build_nonexistent_services_for
- def self.list_nonexistent_services_for(scope)
+ # Returns a list of service types that do not exist in the given scope.
+ # Example: ["AsanaService", ...]
+ def self.nonexistent_services_types_for(scope)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_services_types(include_project_specific: false) - scope.map(&:type)
end
- private_class_method :list_nonexistent_services_for
+ private_class_method :nonexistent_services_types_for
+ # Returns a list of available service names.
+ # Example: ["asana", ...]
def self.available_services_names(include_project_specific: true, include_dev: true)
service_names = services_names
service_names += project_specific_services_names if include_project_specific
@@ -209,40 +219,61 @@ class Service < ApplicationRecord
end
def self.services_names
- SERVICE_NAMES
+ INTEGRATION_NAMES
end
def self.dev_services_names
return [] unless Rails.env.development?
- DEV_SERVICE_NAMES
+ DEV_INTEGRATION_NAMES
end
def self.project_specific_services_names
- PROJECT_SPECIFIC_SERVICE_NAMES
+ PROJECT_SPECIFIC_INTEGRATION_NAMES
end
+ # Returns a list of available service types.
+ # Example: ["AsanaService", ...]
def self.available_services_types(include_project_specific: true, include_dev: true)
available_services_names(include_project_specific: include_project_specific, include_dev: include_dev).map do |service_name|
- "#{service_name}_service".camelize
+ service_name_to_type(service_name)
end
end
+ # Returns the model for the given service name.
+ # Example: "asana" => Integrations::Asana
+ def self.service_name_to_model(name)
+ type = service_name_to_type(name)
+ service_type_to_model(type)
+ end
+
+ # Returns the STI type for the given service name.
+ # Example: "asana" => "AsanaService"
+ def self.service_name_to_type(name)
+ "#{name}_service".camelize
+ end
+
+ # Returns the model for the given STI type.
+ # Example: "AsanaService" => Integrations::Asana
+ def self.service_type_to_model(type)
+ Gitlab::Integrations::StiType.new.cast(type).constantize
+ end
+ private_class_method :service_type_to_model
+
def self.build_from_integration(integration, project_id: nil, group_id: nil)
- service = integration.dup
+ new_integration = integration.dup
if integration.supports_data_fields?
data_fields = integration.data_fields.dup
- data_fields.service = service
+ data_fields.integration = new_integration
end
- service.template = false
- service.instance = false
- service.project_id = project_id
- service.group_id = group_id
- service.inherit_from_id = integration.id if integration.instance? || integration.group
- service.active = false if service.invalid?
- service
+ new_integration.template = false
+ new_integration.instance = false
+ new_integration.project_id = project_id
+ new_integration.group_id = group_id
+ new_integration.inherit_from_id = integration.id if integration.instance_level? || integration.group_level?
+ new_integration
end
def self.instance_exists_for?(type)
@@ -269,7 +300,7 @@ class Service < ApplicationRecord
private_class_method :instance_level_integration
def self.create_from_active_default_integrations(scope, association, with_templates: false)
- group_ids = scope.ancestors.select(:id)
+ group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
@@ -340,7 +371,7 @@ class Service < ApplicationRecord
# Expose a list of fields in the JSON endpoint.
#
- # This list is used in `Service#as_json(only: json_fields)`.
+ # This list is used in `Integration#as_json(only: json_fields)`.
def json_fields
%w[active]
end
@@ -407,16 +438,24 @@ class Service < ApplicationRecord
{ success: result.present?, result: result }
end
- # Disable test for instance-level and group-level services.
+ # Disable test for instance-level and group-level integrations.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
- !instance? && !group_id
+ !(instance_level? || group_level?)
end
def project_level?
project_id.present?
end
+ def group_level?
+ group_id.present?
+ end
+
+ def instance_level?
+ instance?
+ end
+
def parent
project || group
end
@@ -424,7 +463,7 @@ class Service < ApplicationRecord
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
- # so we need a specific implementation for service properties.
+ # so we need a specific implementation for integration properties.
# This allows to track changes to properties set with the accessor methods,
# but not direct manipulation of properties hash.
def updated_properties
@@ -452,12 +491,21 @@ class Service < ApplicationRecord
private
+ # Ancestors sorted by hierarchy depth in bottom-top order.
+ def self.sorted_ancestors(scope)
+ if scope.root_ancestor.use_traversal_ids?
+ Namespace.from(scope.ancestors(hierarchy_order: :asc))
+ else
+ scope.ancestors
+ end
+ end
+
def validate_is_instance_or_template
- errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
+ errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level?
end
def validate_belongs_to_project_or_group
- errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
+ errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level?
end
def validate_recipients?
@@ -465,4 +513,4 @@ class Service < ApplicationRecord
end
end
-Service.prepend_if_ee('EE::Service')
+Integration.prepend_mod_with('Integration')
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
new file mode 100644
index 00000000000..7949563a1dc
--- /dev/null
+++ b/app/models/integrations/asana.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'asana'
+
+module Integrations
+ class Asana < Integration
+ include ActionView::Helpers::UrlHelper
+
+ prop_accessor :api_key, :restrict_to_branch
+ validates :api_key, presence: true, if: :activated?
+
+ def title
+ 'Asana'
+ end
+
+ def description
+ s_('AsanaService|Add commit messages as comments to Asana tasks.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'asana'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_key',
+ title: 'API key',
+ help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
+ # Example Personal Access Token from Asana docs
+ placeholder: '0/68a9e79b868c6789e79a124c30b0',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ title: 'Restrict to branch (optional)',
+ help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
+ }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def client
+ @_client ||= begin
+ ::Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ # check the branch restriction is poplulated and branch is not included
+ branch = Gitlab::Git.ref_name(data[:ref])
+ branch_restriction = restrict_to_branch.to_s
+ if branch_restriction.present? && branch_restriction.index(branch).nil?
+ return
+ end
+
+ user = data[:user_name]
+ project_name = project.full_name
+
+ data[:commits].each do |commit|
+ push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
+ check_commit(commit[:message], push_msg)
+ end
+ end
+
+ def check_commit(message, push_msg)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/{project_gid}/{task_gid}
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = ::Asana::Resources::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue StandardError => e
+ log_error(e.message)
+ next
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
new file mode 100644
index 00000000000..6a36045330a
--- /dev/null
+++ b/app/models/integrations/assembla.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Assembla < Integration
+ prop_accessor :token, :subdomain
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Assembla'
+ end
+
+ def description
+ _('Manage projects.')
+ end
+
+ def self.to_param
+ 'assembla'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'subdomain', placeholder: '' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
+ Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ end
+ end
+end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
new file mode 100644
index 00000000000..82111c7322e
--- /dev/null
+++ b/app/models/integrations/bamboo.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Bamboo < CiService
+ include ActionView::Helpers::UrlHelper
+ include ReactiveService
+
+ prop_accessor :bamboo_url, :build_key, :username, :password
+
+ validates :bamboo_url, presence: true, public_url: true, if: :activated?
+ validates :build_key, presence: true, if: :activated?
+ validates :username,
+ presence: true,
+ if: ->(service) { service.activated? && service.password }
+ validates :password,
+ presence: true,
+ if: ->(service) { service.activated? && service.username }
+
+ attr_accessor :response
+
+ after_save :compose_service_hook, if: :activated?
+ before_update :reset_password
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.save
+ end
+
+ def reset_password
+ if bamboo_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
+
+ def title
+ s_('BambooService|Atlassian Bamboo')
+ end
+
+ def description
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
+ s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ end
+
+ def self.to_param
+ 'bamboo'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'bamboo_url',
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('https://bamboo.example.com'),
+ help: s_('BambooService|Bamboo service root URL.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_key',
+ placeholder: s_('KEY'),
+ help: s_('BambooService|Bamboo build plan key.'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ help: s_('BambooService|The user with API access to the Bamboo server.')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
+ ]
+ end
+
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
+ end
+
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action", { buildKey: build_key })
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
+
+ private
+
+ def get_build_result(response)
+ return if response&.code != 200
+
+ # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
+ result = response.dig('results', 'results', 'result')
+
+ # In case of multiple results, arbitrarily assume the last one is the most relevant.
+ return result.last if result.is_a?(Array)
+
+ result
+ end
+
+ def read_build_page(response)
+ result = get_build_result(response)
+ key =
+ if result.blank?
+ # If actual build link can't be determined, send user to build summary page.
+ build_key
+ else
+ # If actual build link is available, go to build result page.
+ result.dig('planResultKey', 'key')
+ end
+
+ build_url("browse/#{key}")
+ end
+
+ def read_commit_status(response)
+ return :error unless response && (response.code == 200 || response.code == 404)
+
+ result = get_build_result(response)
+ status =
+ if result.blank?
+ 'Pending'
+ else
+ result.dig('buildState')
+ end
+
+ return :error unless status.present?
+
+ if status.include?('Success')
+ 'success'
+ elsif status.include?('Failed')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
+ def try_get_path(path, query_params = {})
+ params = build_get_params(query_params)
+ params[:extra_log_info] = { project_id: project_id }
+
+ Gitlab::HTTP.try_get(build_url(path), params)
+ end
+
+ def get_path(path, query_params = {})
+ Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
+ end
+
+ def build_url(path)
+ Gitlab::Utils.append_path(bamboo_url, path)
+ end
+
+ def build_get_params(query_params)
+ params = { verify: false, query: query_params }
+ return params if username.blank? && password.blank?
+
+ query_params[:os_authType] = 'basic'
+ params[:basic_auth] = basic_auth
+ params
+ end
+
+ def basic_auth
+ { username: username, password: password }
+ end
+ end
+end
diff --git a/app/models/integrations/builds_email.rb b/app/models/integrations/builds_email.rb
new file mode 100644
index 00000000000..2628848667e
--- /dev/null
+++ b/app/models/integrations/builds_email.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
+# https://gitlab.com/gitlab-org/gitlab/-/issues/331064
+module Integrations
+ class BuildsEmail < Integration
+ def self.to_param
+ 'builds_email'
+ end
+
+ def self.supported_events
+ %w[]
+ end
+ end
+end
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
new file mode 100644
index 00000000000..eede3d00307
--- /dev/null
+++ b/app/models/integrations/campfire.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Campfire < Integration
+ prop_accessor :token, :subdomain, :room
+ validates :token, presence: true, if: :activated?
+
+ def title
+ 'Campfire'
+ end
+
+ def description
+ 'Send notifications about push events to Campfire chat rooms.'
+ end
+
+ def self.to_param
+ 'campfire'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '', required: true },
+ { type: 'text', name: 'subdomain', placeholder: '' },
+ { type: 'text', name: 'room', placeholder: '' }
+ ]
+ end
+
+ def self.supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ message = build_message(data)
+ speak(self.room, message, auth)
+ end
+
+ private
+
+ def base_uri
+ @base_uri ||= "https://#{subdomain}.campfirenow.com"
+ end
+
+ def auth
+ # use a dummy password, as explained in the Campfire API doc:
+ # https://github.com/basecamp/campfire-api#authentication
+ @auth ||= {
+ basic_auth: {
+ username: token,
+ password: 'X'
+ }
+ }
+ end
+
+ # Post a message into a room, returns the message Hash in case of success.
+ # Returns nil otherwise.
+ # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+ def speak(room_name, message, auth)
+ room = rooms(auth).find { |r| r["name"] == room_name }
+ return unless room
+
+ path = "/room/#{room["id"]}/speak.json"
+ body = {
+ body: {
+ message: {
+ type: 'TextMessage',
+ body: message
+ }
+ }
+ }
+ res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
+ res.code == 201 ? res : nil
+ end
+
+ # Returns a list of rooms, or [].
+ # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+ def rooms(auth)
+ res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
+ res.code == 200 ? res["rooms"] : []
+ end
+
+ def build_message(push)
+ ref = Gitlab::Git.ref_name(push[:ref])
+ before = push[:before]
+ after = push[:after]
+
+ message = []
+ message << "[#{project.full_name}] "
+ message << "#{push[:user_name]} "
+
+ if Gitlab::Git.blank_ref?(before)
+ message << "pushed new branch #{ref} \n"
+ elsif Gitlab::Git.blank_ref?(after)
+ message << "removed branch #{ref} \n"
+ else
+ message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
+ message << "#{project.web_url}/compare/#{before}...#{after}"
+ end
+
+ message.join
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/alert_message.rb b/app/models/integrations/chat_message/alert_message.rb
new file mode 100644
index 00000000000..ef0579124fe
--- /dev/null
+++ b/app/models/integrations/chat_message/alert_message.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class AlertMessage < BaseMessage
+ attr_reader :title
+ attr_reader :alert_url
+ attr_reader :severity
+ attr_reader :events
+ attr_reader :status
+ attr_reader :started_at
+
+ def initialize(params)
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @title = params.dig(:object_attributes, :title)
+ @alert_url = params.dig(:object_attributes, :url)
+ @severity = params.dig(:object_attributes, :severity)
+ @events = params.dig(:object_attributes, :events)
+ @status = params.dig(:object_attributes, :status)
+ @started_at = params.dig(:object_attributes, :started_at)
+ end
+
+ def attachments
+ [{
+ title: title,
+ title_link: alert_url,
+ color: attachment_color,
+ fields: attachment_fields
+ }]
+ end
+
+ def message
+ "Alert firing in #{project_name}"
+ end
+
+ private
+
+ def attachment_color
+ "#C95823"
+ end
+
+ def attachment_fields
+ [
+ {
+ title: "Severity",
+ value: severity.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Events",
+ value: events,
+ short: true
+ },
+ {
+ title: "Status",
+ value: status.to_s.humanize,
+ short: true
+ },
+ {
+ title: "Start time",
+ value: format_time(started_at),
+ short: true
+ }
+ ]
+ end
+
+ # This formats time into the following format
+ # April 23rd, 2020 1:06AM UTC
+ def format_time(time)
+ time = Time.zone.parse(time.to_s)
+ time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/base_message.rb b/app/models/integrations/chat_message/base_message.rb
new file mode 100644
index 00000000000..2f70384d3b9
--- /dev/null
+++ b/app/models/integrations/chat_message/base_message.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class BaseMessage
+ RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
+
+ attr_reader :markdown
+ attr_reader :user_full_name
+ attr_reader :user_name
+ attr_reader :user_avatar
+ attr_reader :project_name
+ attr_reader :project_url
+
+ def initialize(params)
+ @markdown = params[:markdown] || false
+ @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
+ @project_url = params.dig(:project, :web_url) || params[:project_url]
+ @user_full_name = params.dig(:user, :name) || params[:user_full_name]
+ @user_name = params.dig(:user, :username) || params[:user_name]
+ @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
+ end
+
+ def user_combined_name
+ if user_full_name.present?
+ "#{user_full_name} (#{user_name})"
+ else
+ user_name
+ end
+ end
+
+ def summary
+ return message if markdown
+
+ format(message)
+ end
+
+ def pretext
+ summary
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ raise NotImplementedError
+ end
+
+ def activity
+ raise NotImplementedError
+ end
+
+ private
+
+ def message
+ raise NotImplementedError
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
+ end
+
+ def format_relative_links(string)
+ string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
+ end
+
+ def attachment_color
+ '#345'
+ end
+
+ def link(text, url)
+ "[#{text}](#{url})"
+ end
+
+ def pretty_duration(seconds)
+ parse_string =
+ if duration < 1.hour
+ '%M:%S'
+ else
+ '%H:%M:%S'
+ end
+
+ Time.at(seconds).utc.strftime(parse_string)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/deployment_message.rb b/app/models/integrations/chat_message/deployment_message.rb
new file mode 100644
index 00000000000..c4f3bf9610d
--- /dev/null
+++ b/app/models/integrations/chat_message/deployment_message.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class DeploymentMessage < BaseMessage
+ attr_reader :commit_title
+ attr_reader :commit_url
+ attr_reader :deployable_id
+ attr_reader :deployable_url
+ attr_reader :environment
+ attr_reader :short_sha
+ attr_reader :status
+ attr_reader :user_url
+
+ def initialize(data)
+ super
+
+ @commit_title = data[:commit_title]
+ @commit_url = data[:commit_url]
+ @deployable_id = data[:deployable_id]
+ @deployable_url = data[:deployable_url]
+ @environment = data[:environment]
+ @short_sha = data[:short_sha]
+ @status = data[:status]
+ @user_url = data[:user_url]
+ end
+
+ def attachments
+ [{
+ text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
+ color: color
+ }]
+ end
+
+ def activity
+ {}
+ end
+
+ private
+
+ def message
+ if running?
+ "Starting deploy to #{environment}"
+ else
+ "Deploy to #{environment} #{humanized_status}"
+ end
+ end
+
+ def color
+ case status
+ when 'success'
+ 'good'
+ when 'canceled'
+ 'warning'
+ when 'failed'
+ 'danger'
+ else
+ '#334455'
+ end
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def deployment_link
+ link("##{deployable_id}", deployable_url)
+ end
+
+ def user_link
+ link(user_combined_name, user_url)
+ end
+
+ def commit_link
+ link(short_sha, commit_url)
+ end
+
+ def humanized_status
+ status == 'success' ? 'succeeded' : status
+ end
+
+ def running?
+ status == 'running'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
new file mode 100644
index 00000000000..5fa6bd4090f
--- /dev/null
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class IssueMessage < BaseMessage
+ attr_reader :title
+ attr_reader :issue_iid
+ attr_reader :issue_url
+ attr_reader :action
+ attr_reader :state
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @issue_iid = obj_attr[:iid]
+ @issue_url = obj_attr[:url]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @description = obj_attr[:description] || ''
+ end
+
+ def attachments
+ return [] unless opened_issue?
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "Issue #{state} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: issue_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
+ end
+
+ def opened_issue?
+ action == 'open'
+ end
+
+ def description_message
+ [{
+ title: issue_title,
+ title_link: issue_url,
+ text: format(description),
+ color: '#C95823'
+ }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def issue_link
+ link(issue_title, issue_url)
+ end
+
+ def issue_title
+ "#{Issue.reference_prefix}#{issue_iid} #{title}"
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/merge_message.rb b/app/models/integrations/chat_message/merge_message.rb
new file mode 100644
index 00000000000..d2f48699f50
--- /dev/null
+++ b/app/models/integrations/chat_message/merge_message.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class MergeMessage < BaseMessage
+ attr_reader :merge_request_iid
+ attr_reader :source_branch
+ attr_reader :target_branch
+ attr_reader :action
+ attr_reader :state
+ attr_reader :title
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @merge_request_iid = obj_attr[:iid]
+ @source_branch = obj_attr[:source_branch]
+ @target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
+ @state = obj_attr[:state]
+ @title = format_title(obj_attr[:title])
+ end
+
+ def attachments
+ []
+ end
+
+ def activity
+ {
+ title: "Merge request #{state_or_action_text} by #{user_combined_name}",
+ subtitle: "in #{project_link}",
+ text: merge_request_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def format_title(title)
+ '*' + title.lines.first.chomp + '*'
+ end
+
+ def message
+ merge_request_message
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def merge_request_message
+ "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
+ end
+
+ def merge_request_link
+ link(merge_request_title, merge_request_url)
+ end
+
+ def merge_request_title
+ "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
+ end
+
+ def merge_request_url
+ "#{project_url}/-/merge_requests/#{merge_request_iid}"
+ end
+
+ def state_or_action_text
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/note_message.rb b/app/models/integrations/chat_message/note_message.rb
new file mode 100644
index 00000000000..96675d2b27c
--- /dev/null
+++ b/app/models/integrations/chat_message/note_message.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class NoteMessage < BaseMessage
+ attr_reader :note
+ attr_reader :note_url
+ attr_reader :title
+ attr_reader :target
+
+ def initialize(params)
+ super
+
+ params = HashWithIndifferentAccess.new(params)
+ obj_attr = params[:object_attributes]
+ @note = obj_attr[:note]
+ @note_url = obj_attr[:url]
+ @target, @title = case obj_attr[:noteable_type]
+ when "Commit"
+ create_commit_note(params[:commit])
+ when "Issue"
+ create_issue_note(params[:issue])
+ when "MergeRequest"
+ create_merge_note(params[:merge_request])
+ when "Snippet"
+ create_snippet_note(params[:snippet])
+ end
+ end
+
+ def attachments
+ return note if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
+ subtitle: "in #{project_link}",
+ text: formatted_title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
+ end
+
+ def format_title(title)
+ title.lines.first.chomp
+ end
+
+ def formatted_title
+ format_title(title)
+ end
+
+ def create_issue_note(issue)
+ ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
+ end
+
+ def create_commit_note(commit)
+ commit_sha = Commit.truncate_sha(commit[:id])
+
+ ["commit #{commit_sha}", commit[:message]]
+ end
+
+ def create_merge_note(merge_request)
+ ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
+ end
+
+ def create_snippet_note(snippet)
+ ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
+ end
+
+ def description_message
+ [{ text: format(note), color: attachment_color }]
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
new file mode 100644
index 00000000000..a0f6f582e4c
--- /dev/null
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PipelineMessage < BaseMessage
+ MAX_VISIBLE_JOBS = 10
+
+ attr_reader :user
+ attr_reader :ref_type
+ attr_reader :ref
+ attr_reader :status
+ attr_reader :detailed_status
+ attr_reader :duration
+ attr_reader :finished_at
+ attr_reader :pipeline_id
+ attr_reader :failed_stages
+ attr_reader :failed_jobs
+
+ attr_reader :project
+ attr_reader :commit
+ attr_reader :committer
+ attr_reader :pipeline
+
+ def initialize(data)
+ super
+
+ @user = data[:user]
+ @user_name = data.dig(:user, :username) || 'API'
+
+ pipeline_attributes = data[:object_attributes]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @detailed_status = pipeline_attributes[:detailed_status]
+ @duration = pipeline_attributes[:duration].to_i
+ @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
+ @pipeline_id = pipeline_attributes[:id]
+
+ # Get list of jobs that have actually failed (after exhausting all retries)
+ @failed_jobs = actually_failed_jobs(Array(data[:builds]))
+ @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
+
+ @project = Project.find(data[:project][:id])
+ @commit = project.commit_by(oid: data[:commit][:id])
+ @committer = commit.committer
+ @pipeline = Ci::Pipeline.find(pipeline_id)
+ end
+
+ def pretext
+ ''
+ end
+
+ def attachments
+ return message if markdown
+
+ [{
+ fallback: format(message),
+ color: attachment_color,
+ author_name: user_combined_name,
+ author_icon: user_avatar,
+ author_link: author_url,
+ title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
+ {
+ pipeline_id: pipeline_id,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ },
+ title_link: pipeline_url,
+ fields: attachments_fields,
+ footer: project.name,
+ footer_icon: project.avatar_url(only_path: false),
+ ts: finished_at
+ }]
+ end
+
+ def activity
+ {
+ title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
+ {
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status
+ },
+ subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
+ text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
+ image: user_avatar || ''
+ }
+ end
+
+ private
+
+ def actually_failed_jobs(builds)
+ succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
+
+ failed_jobs = builds.select do |build|
+ # Select jobs which doesn't have a successful retry
+ build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
+ end
+
+ failed_jobs.uniq { |job| job[:name] }.reverse
+ end
+
+ def failed_stages_field
+ {
+ title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
+ short: true
+ }
+ end
+
+ def failed_jobs_field
+ {
+ title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
+ value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
+ short: true
+ }
+ end
+
+ def yaml_error_field
+ {
+ title: s_("ChatMessage|Invalid CI config YAML file"),
+ value: pipeline.yaml_errors,
+ short: false
+ }
+ end
+
+ def attachments_fields
+ fields = [
+ {
+ title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
+ value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
+ short: true
+ },
+ {
+ title: s_("ChatMessage|Commit"),
+ value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
+ short: true
+ }
+ ]
+
+ fields << failed_stages_field if failed_stages.any?
+ fields << failed_jobs_field if failed_jobs.any?
+ fields << yaml_error_field if pipeline.has_yaml_errors?
+
+ fields
+ end
+
+ def message
+ s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
+ {
+ project_link: project_link,
+ pipeline_link: pipeline_link,
+ ref_type: ref_type,
+ ref_link: ref_link,
+ user_combined_name: user_combined_name,
+ humanized_status: humanized_status,
+ duration: pretty_duration(duration)
+ }
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
+ when 'failed'
+ s_("ChatMessage|has failed")
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/-/commits/#{ref}"
+ end
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_url
+ project.web_url
+ end
+
+ def project_link
+ "[#{project.name}](#{project_url})"
+ end
+
+ def pipeline_failed_jobs_url
+ "#{project_url}/-/pipelines/#{pipeline_id}/failures"
+ end
+
+ def pipeline_url
+ if failed_jobs.any?
+ pipeline_failed_jobs_url
+ else
+ "#{project_url}/-/pipelines/#{pipeline_id}"
+ end
+ end
+
+ def pipeline_link
+ "[##{pipeline_id}](#{pipeline_url})"
+ end
+
+ def job_url(job)
+ "#{project_url}/-/jobs/#{job[:id]}"
+ end
+
+ def job_link(job)
+ "[#{job[:name]}](#{job_url(job)})"
+ end
+
+ def failed_jobs_links
+ failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
+ truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
+
+ failed_links = failed.map { |job| job_link(job) }
+
+ unless truncated.blank?
+ failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
+ count: truncated.size,
+ pipeline_failed_jobs_url: pipeline_failed_jobs_url
+ }
+ end
+
+ failed_links.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def stage_link(stage)
+ # All stages link to the pipeline page
+ "[#{stage}](#{pipeline_url})"
+ end
+
+ def failed_stages_links
+ failed_stages.map { |s| stage_link(s) }.join(I18n.t(:'support.array.words_connector'))
+ end
+
+ def commit_url
+ Gitlab::UrlBuilder.build(commit)
+ end
+
+ def commit_link
+ "[#{commit.title}](#{commit_url})"
+ end
+
+ def author_url
+ return unless user && committer
+
+ Gitlab::UrlBuilder.build(committer)
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/push_message.rb b/app/models/integrations/chat_message/push_message.rb
new file mode 100644
index 00000000000..0952986e923
--- /dev/null
+++ b/app/models/integrations/chat_message/push_message.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class PushMessage < BaseMessage
+ attr_reader :after
+ attr_reader :before
+ attr_reader :commits
+ attr_reader :ref
+ attr_reader :ref_type
+
+ def initialize(params)
+ super
+
+ @after = params[:after]
+ @before = params[:before]
+ @commits = params.fetch(:commits, [])
+ @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
+ @ref = Gitlab::Git.ref_name(params[:ref])
+ end
+
+ def attachments
+ return [] if new_branch? || removed_branch?
+ return commit_messages if markdown
+
+ commit_message_attachments
+ end
+
+ def activity
+ {
+ title: humanized_action(short: true),
+ subtitle: "in #{project_link}",
+ text: compare_link,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def humanized_action(short: false)
+ action, ref_link, target_link = compose_action_details
+ text = [user_combined_name, action, ref_type, ref_link]
+ text << target_link unless short
+ text.join(' ')
+ end
+
+ def message
+ humanized_action
+ end
+
+ def format(string)
+ Slack::Messenger::Util::LinkFormatter.format(string)
+ end
+
+ def commit_messages
+ commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
+ end
+
+ def commit_message_attachments
+ [{ text: format(commit_messages), color: attachment_color }]
+ end
+
+ def compose_commit_message(commit)
+ author = commit[:author][:name]
+ id = Commit.truncate_sha(commit[:id])
+ title = commit[:title]
+
+ url = commit[:url]
+
+ "[#{id}](#{url}): #{title} - #{author}"
+ end
+
+ def new_branch?
+ Gitlab::Git.blank_ref?(before)
+ end
+
+ def removed_branch?
+ Gitlab::Git.blank_ref?(after)
+ end
+
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
+ end
+
+ def compare_url
+ "#{project_url}/compare/#{before}...#{after}"
+ end
+
+ def ref_link
+ "[#{ref}](#{ref_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def compare_link
+ "[Compare changes](#{compare_url})"
+ end
+
+ def compose_action_details
+ if new_branch?
+ ['pushed new', ref_link, "to #{project_link}"]
+ elsif removed_branch?
+ ['removed', ref, "from #{project_link}"]
+ else
+ ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
+ end
+ end
+
+ def attachment_color
+ '#345'
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/chat_message/wiki_page_message.rb b/app/models/integrations/chat_message/wiki_page_message.rb
new file mode 100644
index 00000000000..9b5275b8c03
--- /dev/null
+++ b/app/models/integrations/chat_message/wiki_page_message.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Integrations
+ module ChatMessage
+ class WikiPageMessage < BaseMessage
+ attr_reader :title
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ super
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:message]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ return description if markdown
+
+ description_message
+ end
+
+ def activity
+ {
+ title: "#{user_combined_name} #{action} #{wiki_page_link}",
+ subtitle: "in #{project_link}",
+ text: title,
+ image: user_avatar
+ }
+ end
+
+ private
+
+ def message
+ "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
new file mode 100644
index 00000000000..30f73496993
--- /dev/null
+++ b/app/models/integrations/confluence.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Confluence < Integration
+ include ActionView::Helpers::UrlHelper
+
+ VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
+ VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
+ VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
+
+ prop_accessor :confluence_url
+
+ validates :confluence_url, presence: true, if: :activated?
+ validate :validate_confluence_url_is_cloud, if: :activated?
+
+ after_commit :cache_project_has_confluence
+
+ def self.to_param
+ 'confluence'
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def title
+ s_('ConfluenceService|Confluence Workspace')
+ end
+
+ def description
+ s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
+ end
+
+ def help
+ return unless project&.wiki_enabled?
+
+ if activated?
+ wiki_url = project.wiki.web_url
+
+ s_(
+ 'ConfluenceService|Your GitLab wiki is still available at %{wiki_link}. To re-enable the link to the GitLab wiki, disable this integration.' %
+ { wiki_link: link_to(wiki_url, wiki_url) }
+ ).html_safe
+ else
+ s_('ConfluenceService|Link to a Confluence Workspace from the sidebar. Enabling this integration replaces the "Wiki" sidebar link with a link to the Confluence Workspace. The GitLab wiki is still available at the original URL.').html_safe
+ end
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'confluence_url',
+ title: s_('Confluence Cloud Workspace URL'),
+ placeholder: 'https://example.atlassian.net/wiki',
+ required: true
+ }
+ ]
+ end
+
+ def can_test?
+ false
+ end
+
+ private
+
+ def validate_confluence_url_is_cloud
+ unless confluence_uri_valid?
+ errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net')
+ end
+ end
+
+ def confluence_uri_valid?
+ return false unless confluence_url
+
+ uri = URI.parse(confluence_url)
+
+ (uri.scheme&.match(VALID_SCHEME_MATCH) &&
+ uri.host&.match(VALID_HOST_MATCH) &&
+ uri.path&.match(VALID_PATH_MATCH)).present?
+
+ rescue URI::InvalidURIError
+ false
+ end
+
+ def cache_project_has_confluence
+ return unless project && !project.destroyed?
+
+ project.project_setting.save! unless project.project_setting.persisted?
+ project.project_setting.update_column(:has_confluence, active?)
+ end
+ end
+end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
new file mode 100644
index 00000000000..dd4b0664d52
--- /dev/null
+++ b/app/models/integrations/datadog.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+module Integrations
+ class Datadog < Integration
+ DEFAULT_SITE = 'datadoghq.com'
+ URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
+ URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
+ URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
+
+ SUPPORTED_EVENTS = %w[
+ pipeline job
+ ].freeze
+
+ prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env
+
+ with_options if: :activated? do
+ validates :api_key, presence: true, format: { with: /\A\w+\z/ }
+ validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
+ validates :api_url, public_url: { allow_blank: true }
+ validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
+ validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
+ end
+
+ after_save :compose_service_hook, if: :activated?
+
+ def initialize_properties
+ super
+
+ self.datadog_site ||= DEFAULT_SITE
+ end
+
+ def self.supported_events
+ SUPPORTED_EVENTS
+ end
+
+ def self.default_test_event
+ 'pipeline'
+ end
+
+ def configurable_events
+ [] # do not allow to opt out of required hooks
+ end
+
+ def title
+ 'Datadog'
+ end
+
+ def description
+ 'Trace your GitLab pipelines with Datadog'
+ end
+
+ def help
+ nil
+ end
+
+ def self.to_param
+ 'datadog'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'datadog_site',
+ placeholder: DEFAULT_SITE,
+ help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
+ required: false
+ },
+ {
+ type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ help: '(Advanced) Define the full URL for your Datadog site directly',
+ required: false
+ },
+ {
+ type: 'password',
+ name: 'api_key',
+ title: _('API key'),
+ non_empty_password_title: s_('ProjectService|Enter new API key'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
+ help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'datadog_service',
+ title: 'Service',
+ placeholder: 'gitlab-ci',
+ help: 'Name of this GitLab instance that all data will be tagged with'
+ },
+ {
+ type: 'text',
+ name: 'datadog_env',
+ title: 'Env',
+ help: 'The environment tag that traces will be tagged with'
+ }
+ ]
+ end
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = hook_url
+ hook.save
+ end
+
+ def hook_url
+ url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
+ url = URI.parse(url)
+ url.path = File.join(url.path || '/', api_key)
+ query = { service: datadog_service.presence, env: datadog_env.presence }.compact
+ url.query = query.to_query unless query.empty?
+ url.to_s
+ end
+
+ def api_keys_url
+ return URL_API_KEYS_DOCS unless datadog_site.presence
+
+ sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site)
+ end
+
+ def execute(data)
+ return if project.disabled_services.include?(to_param)
+
+ object_kind = data[:object_kind]
+ object_kind = 'job' if object_kind == 'build'
+ return unless supported_events.include?(object_kind)
+
+ service_hook.execute(data, "#{object_kind} hook")
+ end
+
+ def test(data)
+ begin
+ result = execute(data)
+ return { success: false, result: result[:message] } if result[:http_status] != 200
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result[:message] }
+ end
+ end
+end
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
new file mode 100644
index 00000000000..e277633664f
--- /dev/null
+++ b/app/models/integrations/emails_on_push.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Integrations
+ class EmailsOnPush < Integration
+ include NotificationBranchSelection
+
+ RECIPIENTS_LIMIT = 750
+
+ boolean_accessor :send_from_committer_email
+ boolean_accessor :disable_diffs
+ prop_accessor :recipients, :branches_to_be_notified
+ validates :recipients, presence: true, if: :validate_recipients?
+ validate :number_of_recipients_within_limit, if: :validate_recipients?
+
+ def self.valid_recipients(recipients)
+ recipients.split.select do |recipient|
+ recipient.include?('@')
+ end.uniq(&:downcase)
+ end
+
+ def title
+ s_('EmailsOnPushService|Emails on push')
+ end
+
+ def description
+ s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
+ end
+
+ def self.to_param
+ 'emails_on_push'
+ end
+
+ def self.supported_events
+ %w(push tag_push)
+ end
+
+ def initialize_properties
+ super
+
+ self.branches_to_be_notified = 'all' if branches_to_be_notified.nil?
+ end
+
+ def execute(push_data)
+ return unless supported_events.include?(push_data[:object_kind])
+ return if project.emails_disabled?
+ return unless notify_for_ref?(push_data)
+
+ EmailsOnPushWorker.perform_async(
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
+ )
+ end
+
+ def notify_for_ref?(push_data)
+ return true if push_data[:object_kind] == 'tag_push'
+ return true if push_data.dig(:object_attributes, :tag)
+
+ notify_for_branch?(push_data)
+ end
+
+ def send_from_committer_email?
+ Gitlab::Utils.to_boolean(self.send_from_committer_email)
+ end
+
+ def disable_diffs?
+ Gitlab::Utils.to_boolean(self.disable_diffs)
+ end
+
+ def fields
+ domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
+ [
+ { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
+ help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
+ { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
+ help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
+ {
+ type: 'textarea',
+ name: 'recipients',
+ placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
+ help: s_('EmailsOnPushService|Emails separated by whitespace.')
+ }
+ ]
+ end
+
+ private
+
+ def number_of_recipients_within_limit
+ return if recipients.blank?
+
+ if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
+ errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
+ end
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index af78466e6a9..2077f9bfdbb 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -87,7 +87,8 @@ class Issue < ApplicationRecord
enum issue_type: {
issue: 0,
incident: 1,
- test_case: 2 ## EE-only
+ test_case: 2, ## EE-only
+ requirement: 3 ## EE-only
}
alias_method :issuing_parent, :project
@@ -108,6 +109,7 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
+ scope :order_relative_position_desc, -> { reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
@@ -121,7 +123,7 @@ class Issue < ApplicationRecord
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
scope :with_api_entity_associations, -> {
- preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels,
+ preload(:timelogs, :closed_by, :assignees, :author, :labels,
milestone: { project: [:route, { namespace: :route }] },
project: [:route, { namespace: :route }])
}
@@ -174,8 +176,16 @@ class Issue < ApplicationRecord
state :opened, value: Issue.available_states[:opened]
state :closed, value: Issue.available_states[:closed]
- before_transition any => :closed do |issue|
+ before_transition any => :closed do |issue, transition|
+ args = transition.args
+
issue.closed_at = issue.system_note_timestamp
+
+ next if args.empty?
+
+ next unless args.first.is_a?(User)
+
+ issue.closed_by = args.first
end
before_transition closed: :opened do |issue|
@@ -262,6 +272,18 @@ class Issue < ApplicationRecord
"id DESC")
end
+ # Temporary disable moving null elements because of performance problems
+ # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
+ def check_repositioning_allowed!
+ if blocked_for_repositioning?
+ raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
+ end
+ end
+
+ def blocked_for_repositioning?
+ resource_parent.root_namespace&.issue_repositioning_disabled?
+ end
+
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
@@ -506,4 +528,4 @@ class Issue < ApplicationRecord
end
end
-Issue.prepend_if_ee('EE::Issue')
+Issue.prepend_mod_with('Issue')
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
index a5e1957c096..86523bbd023 100644
--- a/app/models/issue/metrics.rb
+++ b/app/models/issue/metrics.rb
@@ -24,6 +24,10 @@ class Issue::Metrics < ApplicationRecord
private
def issue_assigned_to_list_label?
- issue.labels.any? { |label| label.lists.present? }
+ # Avoid another DB lookup when issue.labels are empty by adding a guard clause here
+ # We can't use issue.labels.empty? because that will cause a `Label Exists?` DB lookup
+ return false if issue.labels.length == 0 # rubocop:disable Style/ZeroLengthPredicate
+
+ issue.labels.includes(:lists).any? { |label| label.lists.present? }
end
end
diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb
index d62f0eb170c..d8fbd49d313 100644
--- a/app/models/issue_assignee.rb
+++ b/app/models/issue_assignee.rb
@@ -8,9 +8,9 @@ class IssueAssignee < ApplicationRecord
validates :assignee, uniqueness: { scope: :issue_id }
- scope :in_projects, ->(project_ids) { joins(:issue).where("issues.project_id in (?)", project_ids) }
+ scope :in_projects, ->(project_ids) { joins(:issue).where(issues: { project_id: project_ids }) }
scope :on_issues, ->(issue_ids) { where(issue_id: issue_ids) }
scope :for_assignee, ->(user) { where(assignee: user) }
end
-IssueAssignee.prepend_if_ee('EE::IssueAssignee')
+IssueAssignee.prepend_mod_with('IssueAssignee')
diff --git a/app/models/issue_link.rb b/app/models/issue_link.rb
index ba97874ed39..920586cc1ba 100644
--- a/app/models/issue_link.rb
+++ b/app/models/issue_link.rb
@@ -46,4 +46,4 @@ class IssueLink < ApplicationRecord
end
end
-IssueLink.prepend_if_ee('EE::IssueLink')
+IssueLink.prepend_mod_with('IssueLink')
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 7483d04aab8..71ecbcf1c1a 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -13,4 +13,4 @@ class Iteration < ApplicationRecord
end
end
-Iteration.prepend_if_ee('::EE::Iteration')
+Iteration.prepend_mod_with('Iteration')
diff --git a/app/models/key.rb b/app/models/key.rb
index 131416d1bee..15b3c460b52 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -147,4 +147,4 @@ class Key < ApplicationRecord
end
end
-Key.prepend_if_ee('EE::Key')
+Key.prepend_mod_with('Key')
diff --git a/app/models/label.rb b/app/models/label.rb
index 26faaa90df3..a46d6bc5c0f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -290,4 +290,4 @@ class Label < ApplicationRecord
end
end
-Label.prepend_if_ee('EE::Label')
+Label.prepend_mod_with('Label')
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 5ae1e88e14e..a466fe69300 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -9,4 +9,7 @@ class LabelLink < ApplicationRecord
validates :target, presence: true, unless: :importing?
validates :label, presence: true, unless: :importing?
+
+ scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
+ scope :with_remove_on_close_labels, -> { joins(:label).where(labels: { remove_on_close: true }) }
end
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
index e90028ce835..19dede36abd 100644
--- a/app/models/label_note.rb
+++ b/app/models/label_note.rb
@@ -79,4 +79,4 @@ class LabelNote < SyntheticNote
end
end
-LabelNote.prepend_if_ee('EE::LabelNote')
+LabelNote.prepend_mod_with('LabelNote')
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index df1ad8ea281..25e90036a53 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -112,4 +112,4 @@ class LegacyDiffNote < Note
end
end
-LegacyDiffNote.prepend_if_ee('EE::LegacyDiffNote')
+LegacyDiffNote.prepend_mod_with('LegacyDiffNote')
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index d60baa299cb..b837b902e2d 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -50,4 +50,4 @@ class LfsObject < ApplicationRecord
end
end
-LfsObject.prepend_if_ee('EE::LfsObject')
+LfsObject.prepend_mod_with('LfsObject')
diff --git a/app/models/list.rb b/app/models/list.rb
index d72afbaee69..fba0e51bdf8 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -49,4 +49,4 @@ class List < ApplicationRecord
end
end
-List.prepend_if_ee('::EE::List')
+List.prepend_mod_with('List')
diff --git a/app/models/member.rb b/app/models/member.rb
index e978552592d..044b662e10f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -84,15 +84,25 @@ class Member < ApplicationRecord
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_blocked = User.arel_table[:state].eq(:blocked)
- user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_blocked)
-
left_join_users
- .where(user_ok)
+ .where(user_is_blocked)
+ .where.not(is_external_invite)
.non_request
.non_minimal_access
.reorder(nil)
end
+ scope :connected_to_user, -> { where.not(user_id: nil) }
+
+ # This scope is exclusively used to get the members
+ # that can possibly have project_authorization records
+ # to projects/groups.
+ scope :authorizable, -> do
+ connected_to_user
+ .non_request
+ .non_minimal_access
+ end
+
# Like active, but without invites. For when a User is required.
scope :active_without_invites_and_requests, -> do
left_join_users
@@ -140,7 +150,8 @@ class Member < ApplicationRecord
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
- Member.from(distinct_members, :members)
+
+ from(distinct_members, :members)
end
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
@@ -560,4 +571,4 @@ class Member < ApplicationRecord
end
end
-Member.prepend_if_ee('EE::Member')
+Member.prepend_mod_with('Member')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 0f9fdd230ff..b22a4fa9ef6 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -101,4 +101,4 @@ class GroupMember < Member
end
end
-GroupMember.prepend_if_ee('EE::GroupMember')
+GroupMember.prepend_mod_with('GroupMember')
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 9a86b3a3fd9..41ecc4cbf01 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -16,7 +16,7 @@ class ProjectMember < Member
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_namespaces, ->(groups) do
joins('INNER JOIN projects ON projects.id = members.source_id')
- .where('projects.namespace_id in (?)', groups.select(:id))
+ .where(projects: { namespace_id: groups.select(:id) })
end
scope :without_project_bots, -> do
@@ -69,7 +69,7 @@ class ProjectMember < Member
end
true
- rescue
+ rescue StandardError
false
end
@@ -154,4 +154,4 @@ class ProjectMember < Member
# rubocop: enable CodeReuse/ServiceClass
end
-ProjectMember.prepend_if_ee('EE::ProjectMember')
+ProjectMember.prepend_mod_with('ProjectMember')
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
index 88db7f63bd9..ba7e4b39989 100644
--- a/app/models/members_preloader.rb
+++ b/app/models/members_preloader.rb
@@ -10,10 +10,11 @@ class MembersPreloader
def preload_all
ActiveRecord::Associations::Preloader.new.preload(members, :user)
ActiveRecord::Associations::Preloader.new.preload(members, :source)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
- ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :webauthn_registrations)
+ ActiveRecord::Associations::Preloader.new.preload(members, :created_by)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :status)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :u2f_registrations)
+ ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn)
end
end
-MembersPreloader.prepend_if_ee('EE::MembersPreloader')
+MembersPreloader.prepend_mod_with('MembersPreloader')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e7f3762b9a3..aaef56418d2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -37,6 +37,7 @@ class MergeRequest < ApplicationRecord
SORTING_PREFERENCE_FIELD = :merge_requests_sort
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
+ 'Ci::CompareMetricsReportsService' => ->(project) { ::Gitlab::Ci::Features.merge_base_pipeline_for_metrics_comparison?(project) },
'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
@@ -381,7 +382,7 @@ class MergeRequest < ApplicationRecord
scope :review_requested_to, ->(user) do
where(
reviewers_subquery
- .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
.exists
)
end
@@ -389,7 +390,7 @@ class MergeRequest < ApplicationRecord
scope :no_review_requested_to, ->(user) do
where(
reviewers_subquery
- .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user.id))
.exists
.not
)
@@ -1367,11 +1368,11 @@ class MergeRequest < ApplicationRecord
def environments_for(current_user, latest: false)
return [] unless diff_head_commit
- envs = EnvironmentsByDeploymentsFinder.new(target_project, current_user,
+ envs = Environments::EnvironmentsByDeploymentsFinder.new(target_project, current_user,
ref: target_branch, commit: diff_head_commit, with_tags: true, find_latest: latest).execute
if source_project
- envs.concat EnvironmentsByDeploymentsFinder.new(source_project, current_user,
+ envs.concat Environments::EnvironmentsByDeploymentsFinder.new(source_project, current_user,
ref: source_branch, commit: diff_head_commit, find_latest: latest).execute
end
@@ -1741,7 +1742,7 @@ class MergeRequest < ApplicationRecord
if project.resolve_outdated_diff_discussions?
MergeRequests::ResolvedDiscussionNotificationService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.execute(self)
end
end
@@ -1899,6 +1900,12 @@ class MergeRequest < ApplicationRecord
diff_stats.map(&:path).include?(project.ci_config_path_or_default)
end
+ def context_commits_diff
+ strong_memoize(:context_commits_diff) do
+ ContextCommitsDiff.new(self)
+ end
+ end
+
private
def missing_report_error(report_type)
@@ -1948,4 +1955,4 @@ class MergeRequest < ApplicationRecord
end
end
-MergeRequest.prepend_if_ee('::EE::MergeRequest')
+MergeRequest.prepend_mod_with('MergeRequest')
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 5c611da0684..b9460afa8e7 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -31,4 +31,4 @@ class MergeRequest::Metrics < ApplicationRecord
end
end
-MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
+MergeRequest::Metrics.prepend_mod_with('MergeRequest::Metrics')
diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb
index 73f8fe77b04..86bf950ae19 100644
--- a/app/models/merge_request_assignee.rb
+++ b/app/models/merge_request_assignee.rb
@@ -6,5 +6,5 @@ class MergeRequestAssignee < ApplicationRecord
validates :assignee, uniqueness: { scope: :merge_request_id }
- scope :in_projects, ->(project_ids) { joins(:merge_request).where("merge_requests.target_project_id in (?)", project_ids) }
+ scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) }
end
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 6f15df1b70f..8abedd26b06 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -16,4 +16,8 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
def self.bulk_insert(*args)
Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
+
+ def path
+ new_path.presence || old_path
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index fb873ddbbab..2dc6796732f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -113,14 +113,29 @@ class MergeRequestDiff < ApplicationRecord
joins(merge_request: :metrics).where(condition)
end
+ # This scope uses LATERAL JOIN to find the most recent MR diff association for the given merge requests.
+ # To avoid joining the merge_requests table, we build an in memory table using the merge request ids.
+ # Example:
+ # SELECT ...
+ # FROM (VALUES (MR_ID_1),(MR_ID_2)) merge_requests (id)
+ # INNER JOIN LATERAL (...)
scope :latest_diff_for_merge_requests, -> (merge_requests) do
- inner_select = MergeRequestDiff
- .default_scoped
- .distinct
- .select("FIRST_VALUE(id) OVER (PARTITION BY merge_request_id ORDER BY created_at DESC) as id")
- .where(merge_request: merge_requests)
+ mrs = Array(merge_requests)
+ return MergeRequestDiff.none if mrs.empty?
- joins("INNER JOIN (#{inner_select.to_sql}) latest_diffs ON latest_diffs.id = merge_request_diffs.id")
+ merge_request_table = MergeRequest.arel_table
+ merge_request_diff_table = MergeRequestDiff.arel_table
+
+ join_query = MergeRequestDiff
+ .where(merge_request_table[:id].eq(merge_request_diff_table[:merge_request_id]))
+ .order(created_at: :desc)
+ .limit(1)
+
+ mr_id_list = mrs.map { |mr| "(#{Integer(mr.id)})" }.join(",")
+
+ MergeRequestDiff
+ .from("(VALUES #{mr_id_list}) merge_requests (id)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{MergeRequestDiff.table_name} ON TRUE")
.includes(:merge_request_diff_commits)
end
@@ -665,10 +680,6 @@ class MergeRequestDiff < ApplicationRecord
opening_external_diff do
collection = merge_request_diff_files
- if options[:include_context_commits]
- collection += merge_request.merge_request_context_commit_diff_files
- end
-
if paths = options[:paths]
collection = collection.where('old_path IN (?) OR new_path IN (?)', paths, paths)
end
@@ -743,7 +754,6 @@ class MergeRequestDiff < ApplicationRecord
end
def reorder_diff_files!
- return unless sort_diffs?
return if sorted? || merge_request_diff_files.empty?
diff_files = sort_diffs(merge_request_diff_files)
@@ -762,14 +772,8 @@ class MergeRequestDiff < ApplicationRecord
end
def sort_diffs(diffs)
- return diffs unless sort_diffs?
-
Gitlab::Diff::FileCollectionSorter.new(diffs).sort
end
-
- def sort_diffs?
- Feature.enabled?(:sort_diffs, project, default_enabled: :yaml)
- end
end
-MergeRequestDiff.prepend_if_ee('EE::MergeRequestDiff')
+MergeRequestDiff.prepend_mod_with('MergeRequestDiff')
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 4cf0e423a15..16090f0ebfa 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -7,7 +7,7 @@ class Milestone < ApplicationRecord
include FromUnion
include Importable
- prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
class Predefined
ALL = [::Timebox::None, ::Timebox::Any, ::Timebox::Started, ::Timebox::Upcoming].freeze
@@ -94,7 +94,7 @@ class Milestone < ApplicationRecord
end
def participants
- User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).distinct
+ User.joins(assigned_issues: :milestone).where(milestones: { id: id }).distinct
end
def self.sort_by_attribute(method)
diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb
index c6b5a967af9..93ad961ca51 100644
--- a/app/models/milestone_release.rb
+++ b/app/models/milestone_release.rb
@@ -19,4 +19,4 @@ class MilestoneRelease < ApplicationRecord
end
end
-MilestoneRelease.prepend_if_ee('EE::MilestoneRelease')
+MilestoneRelease.prepend_mod_with('MilestoneRelease')
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 455429608b4..8f03c6145cb 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -14,6 +14,7 @@ class Namespace < ApplicationRecord
include IgnorableColumns
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
+ include EachBatch
ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'
@@ -88,8 +89,12 @@ class Namespace < ApplicationRecord
after_update :move_dir, if: :saved_change_to_path_or_parent?
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
+ after_commit :expire_child_caches, on: :update, if: -> {
+ Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml) &&
+ saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
+ }
- scope :for_user, -> { where('type IS NULL') }
+ scope :for_user, -> { where(type: nil) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@@ -198,7 +203,7 @@ class Namespace < ApplicationRecord
end
def any_project_has_container_registry_tags?
- all_projects.any?(&:has_container_registry_tags?)
+ all_projects.includes(:container_repositories).any?(&:has_container_registry_tags?)
end
def first_project_with_container_registry_tags
@@ -420,8 +425,22 @@ class Namespace < ApplicationRecord
created_at >= 90.days.ago
end
+ def issue_repositioning_disabled?
+ Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
+ end
+
private
+ def expire_child_caches
+ Namespace.where(id: descendants).each_batch do |namespaces|
+ namespaces.touch_all
+ end
+
+ all_projects.each_batch do |projects|
+ projects.touch_all
+ end
+ end
+
def all_projects_with_pages
if all_projects.pages_metadata_not_migrated.exists?
Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
@@ -490,4 +509,4 @@ class Namespace < ApplicationRecord
end
end
-Namespace.prepend_if_ee('EE::Namespace')
+Namespace.prepend_mod_with('Namespace')
diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb
index a2064e020b3..881b2f3acb3 100644
--- a/app/models/namespace/package_setting.rb
+++ b/app/models/namespace/package_setting.rb
@@ -6,13 +6,15 @@ class Namespace::PackageSetting < ApplicationRecord
PackageSettingNotImplemented = Class.new(StandardError)
- PACKAGES_WITH_SETTINGS = %w[maven].freeze
+ PACKAGES_WITH_SETTINGS = %w[maven generic].freeze
belongs_to :namespace, inverse_of: :package_setting_relation
validates :namespace, presence: true
validates :maven_duplicates_allowed, inclusion: { in: [true, false] }
validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
+ validates :generic_duplicates_allowed, inclusion: { in: [true, false] }
+ validates :generic_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 }
class << self
def duplicates_allowed?(package)
@@ -22,7 +24,7 @@ class Namespace::PackageSetting < ApplicationRecord
duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"]
regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z")
- duplicates_allowed || regex.match?(package.name)
+ duplicates_allowed || regex.match?(package.name) || regex.match?(package.version)
end
end
end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 0c91ae760b2..73061b78637 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -70,4 +70,4 @@ class Namespace::RootStorageStatistics < ApplicationRecord
end
end
-Namespace::RootStorageStatistics.prepend_if_ee('EE::Namespace::RootStorageStatistics')
+Namespace::RootStorageStatistics.prepend_mod_with('Namespace::RootStorageStatistics')
diff --git a/app/models/namespace/traversal_hierarchy.rb b/app/models/namespace/traversal_hierarchy.rb
index 28cf55f7486..093b7dae246 100644
--- a/app/models/namespace/traversal_hierarchy.rb
+++ b/app/models/namespace/traversal_hierarchy.rb
@@ -20,7 +20,7 @@ class Namespace
end
def initialize(root)
- raise StandardError.new('Must specify a root node') if root.parent_id
+ raise StandardError, 'Must specify a root node' if root.parent_id
@root = root
end
@@ -34,20 +34,23 @@ class Namespace
sql = """
UPDATE namespaces
SET traversal_ids = cte.traversal_ids
- FROM (#{recursive_traversal_ids(lock: true)}) as cte
+ FROM (#{recursive_traversal_ids}) as cte
WHERE namespaces.id = cte.id
AND namespaces.traversal_ids <> cte.traversal_ids
"""
- Namespace.connection.exec_query(sql)
+ Namespace.transaction do
+ @root.lock!
+ Namespace.connection.exec_query(sql)
+ end
rescue ActiveRecord::Deadlocked
db_deadlock_counter.increment(source: 'Namespace#sync_traversal_ids!')
raise
end
# Identify all incorrect traversal_ids in the current namespace hierarchy.
- def incorrect_traversal_ids(lock: false)
+ def incorrect_traversal_ids
Namespace
- .joins("INNER JOIN (#{recursive_traversal_ids(lock: lock)}) as cte ON namespaces.id = cte.id")
+ .joins("INNER JOIN (#{recursive_traversal_ids}) as cte ON namespaces.id = cte.id")
.where('namespaces.traversal_ids <> cte.traversal_ids')
end
@@ -58,13 +61,10 @@ class Namespace
#
# Note that the traversal_ids represent a calculated traversal path for the
# namespace and not the value stored within the traversal_ids attribute.
- #
- # Optionally locked with FOR UPDATE to ensure isolation between concurrent
- # updates of the heirarchy.
- def recursive_traversal_ids(lock: false)
+ def recursive_traversal_ids
root_id = Integer(@root.id)
- sql = <<~SQL
+ <<~SQL
WITH RECURSIVE cte(id, traversal_ids, cycle) AS (
VALUES(#{root_id}, ARRAY[#{root_id}], false)
UNION ALL
@@ -74,10 +74,6 @@ class Namespace
)
SELECT id, traversal_ids FROM cte
SQL
-
- sql += ' FOR UPDATE' if lock
-
- sql
end
# This is essentially Namespace#root_ancestor which will soon be rewritten
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index d21f9632e18..75b8169b58e 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -45,4 +45,4 @@ class NamespaceSetting < ApplicationRecord
end
end
-NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
+NamespaceSetting.prepend_mod_with('NamespaceSetting')
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 294ef83b9b4..a1711bc5ee0 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -41,6 +41,7 @@ module Namespaces
UnboundedSearch = Class.new(StandardError)
included do
+ before_update :lock_both_roots, if: -> { sync_traversal_ids? && parent_id_changed? }
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
@@ -52,15 +53,30 @@ module Namespaces
end
def use_traversal_ids?
- Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+
+ traversal_ids.present?
end
def self_and_descendants
- if use_traversal_ids?
- lineage(self)
- else
- super
- end
+ return super unless use_traversal_ids?
+
+ lineage(top: self)
+ end
+
+ def descendants
+ return super unless use_traversal_ids?
+
+ self_and_descendants.where.not(id: id)
+ end
+
+ def ancestors(hierarchy_order: nil)
+ return super() unless use_traversal_ids?
+ return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
+
+ return self.class.none if parent_id.blank?
+
+ lineage(bottom: parent, hierarchy_order: hierarchy_order)
end
private
@@ -75,6 +91,23 @@ module Namespaces
Namespace::TraversalHierarchy.for_namespace(root_ancestor).sync_traversal_ids!
end
+ # Lock the root of the hierarchy we just left, and lock the root of the hierarchy
+ # we just joined. In most cases the two hierarchies will be the same.
+ def lock_both_roots
+ parent_ids = [
+ parent_id_was || self.id,
+ parent_id || self.id
+ ].compact
+
+ roots = Gitlab::ObjectHierarchy
+ .new(Namespace.where(id: parent_ids))
+ .base_and_ancestors
+ .reorder(nil)
+ .where(parent_id: nil)
+
+ Namespace.lock.select(:id).where(id: roots).order(id: :asc).load
+ end
+
# Make sure we drop the STI `type = 'Group'` condition for better performance.
# Logically equivalent so long as hierarchies remain homogeneous.
def without_sti_condition
@@ -82,29 +115,29 @@ module Namespaces
end
# Search this namespace's lineage. Bound inclusively by top node.
- def lineage(top)
- raise UnboundedSearch.new('Must bound search by a top') unless top
+ def lineage(top: nil, bottom: nil, hierarchy_order: nil)
+ raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- without_sti_condition
- .traversal_ids_contains(latest_traversal_ids(top))
- end
+ skope = without_sti_condition
- # traversal_ids are a cached value.
- #
- # The traversal_ids value in a loaded object can become stale when compared
- # to the database value. For example, if you load a hierarchy and then move
- # a group, any previously loaded descendant objects will have out of date
- # traversal_ids.
- #
- # To solve this problem, we never depend on the object's traversal_ids
- # value. We always query the database first with a sub-select for the
- # latest traversal_ids.
- #
- # Note that ActiveRecord will cache query results. You can avoid this by
- # using `Model.uncached { ... }`
- def latest_traversal_ids(namespace = self)
- without_sti_condition.where('id = (?)', namespace)
- .select('traversal_ids as latest_traversal_ids')
+ if top
+ skope = skope.traversal_ids_contains("{#{top.id}}")
+ end
+
+ if bottom
+ skope = skope.where(id: bottom.traversal_ids[0..-1])
+ end
+
+ # The original `with_depth` attribute in ObjectHierarchy increments as you
+ # walk away from the "base" namespace. This direction changes depending on
+ # if you are walking up the ancestors or down the descendants.
+ if hierarchy_order
+ depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
+ skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
+ .order(depth: hierarchy_order)
+ end
+
+ skope
end
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 9da454125eb..560ff861105 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -27,7 +27,7 @@ module Network
@project
.notes
- .where('noteable_type = ?', 'Commit')
+ .where(noteable_type: 'Commit')
.group('notes.commit_id')
.select('notes.commit_id, count(notes.id) as note_count')
.each do |item|
diff --git a/app/models/note.rb b/app/models/note.rb
index 3e560a09fbd..ae4a8859d4d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,6 +107,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
@@ -319,7 +320,7 @@ class Note < ApplicationRecord
return commit if for_commit?
super
- rescue
+ rescue StandardError
# Temp fix to prevent app crash
# if note commit id doesn't exist
nil
@@ -495,7 +496,7 @@ class Note < ApplicationRecord
noteable&.expire_note_etag_cache
end
- def touch(*args)
+ def touch(*args, **kwargs)
# We're not using an explicit transaction here because this would in all
# cases result in all future queries going to the primary, even if no writes
# are performed.
@@ -638,4 +639,4 @@ class Note < ApplicationRecord
end
end
-Note.prepend_if_ee('EE::Note')
+Note.prepend_mod_with('Note')
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 3d049336d44..4323f89865a 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -118,4 +118,4 @@ class NotificationSetting < ApplicationRecord
end
end
-NotificationSetting.prepend_if_ee('EE::NotificationSetting')
+NotificationSetting.prepend_mod_with('NotificationSetting')
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index be3f719ddb3..537543a7ff0 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -97,7 +97,7 @@ module Operations
issues = ::Issue
.select('issues.*, operations_feature_flags_issues.id AS link_id')
.joins(:feature_flag_issues)
- .where('operations_feature_flags_issues.feature_flag_id = ?', id)
+ .where(operations_feature_flags_issues: { feature_flag_id: id })
.order('operations_feature_flags_issues.id ASC')
.includes(preload)
diff --git a/app/models/packages.rb b/app/models/packages.rb
index e14c9290093..19490d23ce4 100644
--- a/app/models/packages.rb
+++ b/app/models/packages.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
module Packages
+ DuplicatePackageError = Class.new(StandardError)
+
def self.table_name_prefix
'packages_'
end
diff --git a/app/models/packages/debian/group_distribution.rb b/app/models/packages/debian/group_distribution.rb
index eea7acacc96..50c1ec9f163 100644
--- a/app/models/packages/debian/group_distribution.rb
+++ b/app/models/packages/debian/group_distribution.rb
@@ -6,4 +6,14 @@ class Packages::Debian::GroupDistribution < ApplicationRecord
end
include Packages::Debian::Distribution
+
+ def packages
+ Packages::Package
+ .for_projects(group.all_projects.public_only)
+ .with_debian_codename(codename)
+ end
+
+ def package_files
+ ::Packages::PackageFile.for_package_ids(packages.select(:id))
+ end
end
diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb
index 22f1008b3b5..5ac60d789b3 100644
--- a/app/models/packages/debian/project_distribution.rb
+++ b/app/models/packages/debian/project_distribution.rb
@@ -5,8 +5,9 @@ class Packages::Debian::ProjectDistribution < ApplicationRecord
:project
end
+ include Packages::Debian::Distribution
+
has_many :publications, class_name: 'Packages::Debian::Publication', inverse_of: :distribution, foreign_key: :distribution_id
has_many :packages, class_name: 'Packages::Package', through: :publications
-
- include Packages::Debian::Distribution
+ has_many :package_files, class_name: 'Packages::PackageFile', through: :packages
end
diff --git a/app/models/packages/go/module.rb b/app/models/packages/go/module.rb
index b38b691ed6c..00d51c21881 100644
--- a/app/models/packages/go/module.rb
+++ b/app/models/packages/go/module.rb
@@ -18,8 +18,8 @@ module Packages
end
def version_by(ref: nil, commit: nil)
- raise ArgumentError.new 'no filter specified' unless ref || commit
- raise ArgumentError.new 'ref and commit are mutually exclusive' if ref && commit
+ raise ArgumentError, 'no filter specified' unless ref || commit
+ raise ArgumentError, 'ref and commit are mutually exclusive' if ref && commit
if commit
return version_by_sha(commit) if commit.is_a? String
diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb
index fd575e6c96c..c442b2416f1 100644
--- a/app/models/packages/go/module_version.rb
+++ b/app/models/packages/go/module_version.rb
@@ -17,15 +17,15 @@ module Packages
delegate :build, to: :@semver, allow_nil: true
def initialize(mod, type, commit, name: nil, semver: nil, ref: nil)
- raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type
- raise ArgumentError.new("mod is required") unless mod
- raise ArgumentError.new("commit is required") unless commit
+ raise ArgumentError, "invalid type '#{type}'" unless VALID_TYPES.include? type
+ raise ArgumentError, "mod is required" unless mod
+ raise ArgumentError, "commit is required" unless commit
if type == :ref
- raise ArgumentError.new("ref is required") unless ref
+ raise ArgumentError, "ref is required" unless ref
elsif type == :pseudo
- raise ArgumentError.new("name is required") unless name
- raise ArgumentError.new("semver is required") unless semver
+ raise ArgumentError, "name is required" unless name
+ raise ArgumentError, "semver is required" unless semver
end
@mod = mod
diff --git a/app/models/packages/helm.rb b/app/models/packages/helm.rb
new file mode 100644
index 00000000000..e021b997bf5
--- /dev/null
+++ b/app/models/packages/helm.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Packages
+ module Helm
+ def self.table_name_prefix
+ 'packages_helm_'
+ end
+ end
+end
diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb
new file mode 100644
index 00000000000..1771003d1f9
--- /dev/null
+++ b/app/models/packages/helm/file_metadatum.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Packages
+ module Helm
+ class FileMetadatum < ApplicationRecord
+ self.primary_key = :package_file_id
+
+ belongs_to :package_file, inverse_of: :helm_file_metadatum
+
+ validates :package_file, presence: true
+ validate :valid_helm_package_type
+
+ validates :channel,
+ presence: true,
+ length: { maximum: 63 },
+ format: { with: Gitlab::Regex.helm_channel_regex }
+
+ validates :metadata,
+ json_schema: { filename: "helm_metadata" }
+
+ private
+
+ def valid_helm_package_type
+ return if package_file&.package&.helm?
+
+ errors.add(:package_file, _('Package type must be Helm'))
+ end
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index e510432be8f..36edf646658 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -6,6 +6,7 @@ class Packages::Package < ApplicationRecord
include Gitlab::Utils::StrongMemoize
DISPLAYABLE_STATUSES = [:default, :error].freeze
+ INSTALLABLE_STATUSES = [:default].freeze
belongs_to :project
belongs_to :creator, class_name: 'User'
@@ -47,8 +48,10 @@ class Packages::Package < ApplicationRecord
validate :package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
+ validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm?
validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget?
+ validates :name, format: { with: Gitlab::Regex.terraform_module_package_name_regex }, if: :terraform_module?
validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package?
validates :name, inclusion: { in: %w[incoming] }, if: :debian_incoming?
validates :version, format: { with: Gitlab::Regex.nuget_version_regex }, if: :nuget?
@@ -56,7 +59,8 @@ class Packages::Package < ApplicationRecord
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :golang?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? }
+ validates :version, format: { with: Gitlab::Regex.prefixed_semver_regex }, if: :helm?
+ validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { composer_tag_version? || npm? || terraform_module? }
validates :version,
presence: true,
@@ -70,10 +74,11 @@ class Packages::Package < ApplicationRecord
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5,
composer: 6, generic: 7, golang: 8, debian: 9,
- rubygems: 10 }
+ rubygems: 10, helm: 11, terraform_module: 12 }
enum status: { default: 0, hidden: 1, processing: 2, error: 3 }
+ scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
scope :with_normalized_pypi_name, ->(name) { where("LOWER(regexp_replace(name, '[-_.]+', '-', 'g')) = ?", name.downcase) }
@@ -81,8 +86,10 @@ class Packages::Package < ApplicationRecord
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
+ scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
+ scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
@@ -110,25 +117,20 @@ class Packages::Package < ApplicationRecord
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
- scope :processed, -> do
- where.not(package_type: :nuget).or(
- where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME)
- )
- end
scope :preload_files, -> { preload(:package_files) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
scope :select_distinct_name, -> { select(:name).distinct }
# Sorting
- scope :order_created, -> { reorder('created_at ASC') }
- scope :order_created_desc, -> { reorder('created_at DESC') }
- scope :order_name, -> { reorder('name ASC') }
- scope :order_name_desc, -> { reorder('name DESC') }
- scope :order_version, -> { reorder('version ASC') }
- scope :order_version_desc, -> { reorder('version DESC') }
- scope :order_type, -> { reorder('package_type ASC') }
- scope :order_type_desc, -> { reorder('package_type DESC') }
+ scope :order_created, -> { reorder(created_at: :asc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_name, -> { reorder(name: :asc) }
+ scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_version, -> { reorder(version: :asc) }
+ scope :order_version_desc, -> { reorder(version: :desc) }
+ scope :order_type, -> { reorder(package_type: :asc) }
+ scope :order_type_desc, -> { reorder(package_type: :desc) }
scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') }
scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') }
scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') }
@@ -137,14 +139,6 @@ class Packages::Package < ApplicationRecord
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
- def self.for_projects(projects)
- unless Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml)
- return none unless projects.any?
- end
-
- where(project_id: projects)
- end
-
def self.only_maven_packages_with_path(path, use_cte: false)
if use_cte && Feature.enabled?(:maven_metadata_by_path_with_optimization_fence, default_enabled: :yaml)
# This is an optimization fence which assumes that looking up the Metadatum record by path (globally)
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 23a7144e2bb..3d8641ca2fa 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -5,7 +5,8 @@ class Packages::PackageFile < ApplicationRecord
delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
- delegate :file_type, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :file_type, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :channel, :metadata, to: :helm_file_metadatum, prefix: :helm
belongs_to :package
@@ -13,9 +14,11 @@ class Packages::PackageFile < ApplicationRecord
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
has_many :pipelines, through: :package_file_build_infos
has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum'
+ has_one :helm_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Helm::FileMetadatum'
accepts_nested_attributes_for :conan_file_metadatum
accepts_nested_attributes_for :debian_file_metadatum
+ accepts_nested_attributes_for :helm_file_metadatum
validates :package, presence: true
validates :file, presence: true
@@ -24,6 +27,7 @@ class Packages::PackageFile < ApplicationRecord
validates :file_name, uniqueness: { scope: :package }, if: -> { package&.pypi? }
scope :recent, -> { order(id: :desc) }
+ scope :for_package_ids, ->(ids) { where(package_id: ids) }
scope :with_file_name, ->(file_name) { where(file_name: file_name) }
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
@@ -41,7 +45,17 @@ class Packages::PackageFile < ApplicationRecord
scope :with_debian_file_type, ->(file_type) do
joins(:debian_file_metadatum)
- .where(packages_debian_file_metadata: { debian_file_type: ::Packages::Debian::FileMetadatum.debian_file_types[file_type] })
+ .where(packages_debian_file_metadata: { file_type: ::Packages::Debian::FileMetadatum.file_types[file_type] })
+ end
+
+ scope :with_debian_component_name, ->(component_name) do
+ joins(:debian_file_metadatum)
+ .where(packages_debian_file_metadata: { component: component_name })
+ end
+
+ scope :with_debian_architecture_name, ->(architecture_name) do
+ joins(:debian_file_metadatum)
+ .where(packages_debian_file_metadata: { architecture: architecture_name })
end
scope :with_conan_package_reference, ->(conan_package_reference) do
@@ -66,4 +80,4 @@ class Packages::PackageFile < ApplicationRecord
end
end
-Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFile')
+Packages::PackageFile.prepend_mod_with('Packages::PackageFile')
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 3285a1f7f4c..17131cd736d 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -50,8 +50,6 @@ module Pages
def zip_source
return unless deployment&.file
- return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project, default_enabled: :yaml)
-
global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
{
@@ -64,17 +62,16 @@ module Pages
}
end
+ # TODO: remove support for legacy storage in 14.3 https://gitlab.com/gitlab-org/gitlab/-/issues/328712
+ # we support this till 14.3 to allow people to still use legacy storage if something goes very wrong
+ # on self-hosted installations, and we'll need some time to fix it
def legacy_source
- raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true)
+ return unless ::Settings.pages.local_store.enabled
{
type: 'file',
path: File.join(project.full_path, 'public/')
}
- rescue LegacyStorageDisabledError => e
- Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
-
- nil
end
end
end
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 90cb8253b52..497f67993ae 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -21,9 +21,7 @@ module Pages
project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
end
- # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524
- # source can only be nil if pages_serve_from_legacy_storage FF is disabled
- # we can remove this filtering once we remove legacy storage
+ # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715
paths = paths.select(&:source)
paths.sort_by(&:prefix).reverse
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 4d60489e599..4668fc265a0 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -311,4 +311,4 @@ class PagesDomain < ApplicationRecord
end
end
-PagesDomain.prepend_if_ee('::EE::PagesDomain')
+PagesDomain.prepend_mod_with('PagesDomain')
diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb
index 411456cc237..8427176fa72 100644
--- a/app/models/pages_domain_acme_order.rb
+++ b/app/models/pages_domain_acme_order.rb
@@ -14,7 +14,7 @@ class PagesDomainAcmeOrder < ApplicationRecord
attr_encrypted :private_key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: true
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index ad2f4525171..732ed0b7bb3 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -55,7 +55,7 @@ class PersonalAccessToken < ApplicationRecord
begin
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
- rescue => ex
+ rescue StandardError => ex
logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
encrypted_token
end
@@ -110,4 +110,4 @@ class PersonalAccessToken < ApplicationRecord
end
end
-PersonalAccessToken.prepend_if_ee('EE::PersonalAccessToken')
+PersonalAccessToken.prepend_mod_with('PersonalAccessToken')
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 6a7f32a5d5f..f3ef04315f8 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -39,4 +39,4 @@ class Plan < ApplicationRecord
end
end
-Plan.prepend_if_ee('EE::Plan')
+Plan.prepend_mod_with('Plan')
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 94992adfd1e..78cddaa1302 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -115,4 +115,4 @@ class PoolRepository < ApplicationRecord
end
end
-PoolRepository.prepend_if_ee('EE::PoolRepository')
+PoolRepository.prepend_mod_with('PoolRepository')
diff --git a/app/models/preloaders/labels_preloader.rb b/app/models/preloaders/labels_preloader.rb
index 427f2869aac..bb3206f5399 100644
--- a/app/models/preloaders/labels_preloader.rb
+++ b/app/models/preloaders/labels_preloader.rb
@@ -31,4 +31,4 @@ module Preloaders
end
end
-Preloaders::LabelsPreloader.prepend_if_ee('EE::Preloaders::LabelsPreloader')
+Preloaders::LabelsPreloader.prepend_mod_with('Preloaders::LabelsPreloader')
diff --git a/app/models/project.rb b/app/models/project.rb
index f03e5293b58..9d572b7e2f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,6 +19,7 @@ class Project < ApplicationRecord
include Presentable
include HasRepository
include HasWiki
+ include HasIntegrations
include CanMoveRepositoryStorage
include Routable
include GroupDescendant
@@ -33,7 +34,6 @@ class Project < ApplicationRecord
include OptionallySearch
include FromUnion
include IgnorableColumns
- include Integration
include Repositories::CanHousekeepRepository
include EachBatch
include GitlabRoutingHelper
@@ -104,16 +104,13 @@ class Project < ApplicationRecord
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
- after_create :create_project_feature, unless: :project_feature
+ after_create -> { create_or_load_association(:project_feature) }
- after_create :create_ci_cd_settings,
- unless: :ci_cd_settings
+ after_create -> { create_or_load_association(:ci_cd_settings) }
- after_create :create_container_expiration_policy,
- unless: :container_expiration_policy
+ after_create -> { create_or_load_association(:container_expiration_policy) }
- after_create :create_pages_metadatum,
- unless: :pages_metadatum
+ after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_update :update_forks_visibility_level
@@ -131,7 +128,41 @@ class Project < ApplicationRecord
after_initialize :use_hashed_storage
after_create :check_repository_absence!
- acts_as_ordered_taggable
+ acts_as_ordered_taggable_on :topics
+ # The 'tag_list' alias and the 'has_many' associations are required during the 'tags -> topics' migration
+ # TODO: eliminate 'tag_list', 'topic_taggings' and 'tags' in the further process of the migration
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331081
+ alias_attribute :tag_list, :topic_list
+ has_many :topic_taggings, -> { includes(:tag).order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ as: :taggable,
+ class_name: 'ActsAsTaggableOn::Tagging',
+ after_add: :dirtify_tag_list,
+ after_remove: :dirtify_tag_list
+ has_many :topics, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ class_name: 'ActsAsTaggableOn::Tag',
+ through: :topic_taggings,
+ source: :tag
+ has_many :tags, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
+ class_name: 'ActsAsTaggableOn::Tag',
+ through: :topic_taggings,
+ source: :tag
+
+ # Overwriting 'topic_list' and 'topic_list=' is necessary to ensure functionality during the background migration [1].
+ # [1] https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61237
+ # TODO: remove 'topic_list' and 'topic_list=' once the background migration is complete
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/331081
+ def topic_list
+ # Return both old topics (context 'tags') and new topics (context 'topics')
+ tag_list_on('tags') + tag_list_on('topics')
+ end
+
+ def topic_list=(new_tags)
+ # Old topics with context 'tags' are added as new topics with context 'topics'
+ super(new_tags)
+
+ # Remove old topics with context 'tags'
+ set_tag_list_on('tags', '')
+ end
attr_accessor :old_path_with_namespace
attr_accessor :template_name
@@ -151,26 +182,26 @@ class Project < ApplicationRecord
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards
- # Project services
- has_one :campfire_service
- has_one :datadog_service
+ # Project integrations
+ has_one :asana_service, class_name: 'Integrations::Asana'
+ has_one :assembla_service, class_name: 'Integrations::Assembla'
+ has_one :bamboo_service, class_name: 'Integrations::Bamboo'
+ has_one :campfire_service, class_name: 'Integrations::Campfire'
+ has_one :confluence_service, class_name: 'Integrations::Confluence'
+ has_one :datadog_service, class_name: 'Integrations::Datadog'
+ has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
has_one :discord_service
has_one :drone_ci_service
- has_one :emails_on_push_service
has_one :ewm_service
has_one :pipelines_email_service
has_one :irker_service
has_one :pivotaltracker_service
- has_one :hipchat_service
has_one :flowdock_service
- has_one :assembla_service
- has_one :asana_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
has_one :slack_service
has_one :buildkite_service
- has_one :bamboo_service
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
@@ -179,7 +210,6 @@ class Project < ApplicationRecord
has_one :youtrack_service
has_one :custom_issue_tracker_service
has_one :bugzilla_service
- has_one :confluence_service
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service
@@ -227,7 +257,7 @@ class Project < ApplicationRecord
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :labels, class_name: 'ProjectLabel'
- has_many :services
+ has_many :integrations
has_many :events
has_many :milestones
has_many :iterations
@@ -338,7 +368,8 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
- has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
+ has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :project
+ has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', inverse_of: :project
has_many :external_pull_requests, inverse_of: :project
@@ -371,6 +402,8 @@ class Project < ApplicationRecord
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
+ has_many :timelogs
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -528,7 +561,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
- scope :with_active_jira_services, -> { joins(:services).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
+ scope :with_active_jira_services, -> { joins(:integrations).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) }
@@ -619,7 +652,7 @@ class Project < ApplicationRecord
mount_uploader :bfg_object_map, AttachmentUploader
def self.with_api_entity_associations
- preload(:project_feature, :route, :tags, :group, namespace: [:route, :owner])
+ preload(:project_feature, :route, :tags, :group, :timelogs, namespace: [:route, :owner])
end
def self.with_web_entity_associations
@@ -832,6 +865,10 @@ class Project < ApplicationRecord
super
end
+ def parent_loaded?
+ association(:namespace).loaded?
+ end
+
def project_setting
super.presence || build_project_setting
end
@@ -1005,7 +1042,7 @@ class Project < ApplicationRecord
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
- latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
+ latest_successful_build_for_ref(job_name, ref) || raise(ActiveRecord::RecordNotFound, "Couldn't find job #{job_name}")
end
def latest_pipeline(ref = default_branch, sha = nil)
@@ -1098,7 +1135,7 @@ class Project < ApplicationRecord
else
super
end
- rescue
+ rescue StandardError
super
end
@@ -1342,7 +1379,7 @@ class Project < ApplicationRecord
return unless has_external_issue_tracker?
- @external_issue_tracker ||= services.external_issue_trackers.first
+ @external_issue_tracker ||= integrations.external_issue_trackers.first
end
def external_references_supported?
@@ -1358,11 +1395,11 @@ class Project < ApplicationRecord
return unless has_external_wiki?
- @external_wiki ||= services.external_wikis.first
+ @external_wiki ||= integrations.external_wikis.first
end
def find_or_initialize_services
- available_services_names = Service.available_services_names - disabled_services
+ available_services_names = Integration.available_services_names - disabled_services
available_services_names.map do |service_name|
find_or_initialize_service(service_name)
@@ -1378,7 +1415,7 @@ class Project < ApplicationRecord
def find_or_initialize_service(name)
return if disabled_services.include?(name)
- find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
+ find_service(integrations, name) || build_from_instance_or_template(name) || build_service(name)
end
# rubocop: disable CodeReuse/ServiceClass
@@ -1391,7 +1428,7 @@ class Project < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def ci_services
- services.where(category: :ci)
+ integrations.where(category: :ci)
end
def ci_service
@@ -1399,7 +1436,7 @@ class Project < ApplicationRecord
end
def monitoring_services
- services.where(category: :monitoring)
+ integrations.where(category: :monitoring)
end
def monitoring_service
@@ -1477,8 +1514,8 @@ class Project < ApplicationRecord
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
- service.async_execute(data)
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
end
end
end
@@ -1488,7 +1525,7 @@ class Project < ApplicationRecord
end
def has_active_services?(hooks_scope = :push_hooks)
- services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
+ integrations.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend
end
def feature_usage
@@ -1560,7 +1597,7 @@ class Project < ApplicationRecord
repository.after_create
true
- rescue => err
+ rescue StandardError => err
Gitlab::ErrorTracking.track_exception(err, project: { id: id, full_path: full_path, disk_path: disk_path })
errors.add(:base, _('Failed to create repository'))
false
@@ -2417,7 +2454,7 @@ class Project < ApplicationRecord
end
def access_request_approvers_to_be_notified
- members.maintainers.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ members.maintainers.connected_to_user.order_recent_sign_in.limit(ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def pages_lookup_path(trim_prefix: nil, domain: nil)
@@ -2529,12 +2566,14 @@ class Project < ApplicationRecord
namespace.root_ancestor.all_projects
.joins(:packages)
.where.not(id: id)
- .merge(Packages::Package.with_name(package_name))
+ .merge(Packages::Package.default_scoped.with_name(package_name))
.exists?
end
- def default_branch_or_master
- default_branch || 'master'
+ def default_branch_or_main
+ return default_branch if default_branch
+
+ Gitlab::DefaultBranch.value(object: self)
end
def ci_config_path_or_default
@@ -2569,6 +2608,16 @@ class Project < ApplicationRecord
Feature.enabled?(:inherited_issuable_templates, self, default_enabled: :yaml)
end
+ def activity_path
+ Gitlab::Routing.url_helpers.activity_project_path(self)
+ end
+
+ def increment_statistic_value(statistic, delta)
+ return if pending_delete?
+
+ ProjectStatistics.increment_statistic(self, statistic, delta)
+ end
+
private
def set_container_registry_access_level
@@ -2591,22 +2640,22 @@ class Project < ApplicationRecord
def build_from_instance_or_template(name)
instance = find_service(services_instances, name)
- return Service.build_from_integration(instance, project_id: id) if instance
+ return Integration.build_from_integration(instance, project_id: id) if instance
template = find_service(services_templates, name)
- return Service.build_from_integration(template, project_id: id) if template
+ return Integration.build_from_integration(template, project_id: id) if template
end
def build_service(name)
- "#{name}_service".classify.constantize.new(project_id: id)
+ Integration.service_name_to_model(name).new(project_id: id)
end
def services_templates
- @services_templates ||= Service.for_template
+ @services_templates ||= Integration.for_template
end
def services_instances
- @services_instances ||= Service.for_instance
+ @services_instances ||= Integration.for_instance
end
def closest_namespace_setting(name)
@@ -2664,7 +2713,7 @@ class Project < ApplicationRecord
def cross_namespace_reference?(from)
case from
when Project
- namespace != from.namespace
+ namespace_id != from.namespace_id
when Namespace
namespace != from
when User
@@ -2743,11 +2792,11 @@ class Project < ApplicationRecord
end
def cache_has_external_wiki
- update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
+ update_column(:has_external_wiki, integrations.external_wikis.any?) if Gitlab::Database.read_write?
end
def cache_has_external_issue_tracker
- update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
+ update_column(:has_external_issue_tracker, integrations.external_issue_trackers.any?) if Gitlab::Database.read_write?
end
def active_runners_with_tags
@@ -2759,4 +2808,4 @@ class Project < ApplicationRecord
end
end
-Project.prepend_if_ee('EE::Project')
+Project.prepend_mod_with('Project')
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 2c3f70654f8..1fed166e4d0 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -31,4 +31,4 @@ class ProjectAuthorization < ApplicationRecord
end
end
-ProjectAuthorization.prepend_if_ee('::EE::ProjectAuthorization')
+ProjectAuthorization.prepend_mod_with('ProjectAuthorization')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 31be0759cd0..c0c2ea42d46 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -33,4 +33,4 @@ class ProjectCiCdSetting < ApplicationRecord
end
end
-ProjectCiCdSetting.prepend_if_ee('EE::ProjectCiCdSetting')
+ProjectCiCdSetting.prepend_mod_with('ProjectCiCdSetting')
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 15f6bedfc2e..eb4ad327438 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -145,4 +145,4 @@ class ProjectFeature < ApplicationRecord
end
end
-ProjectFeature.prepend_if_ee('EE::ProjectFeature')
+ProjectFeature.prepend_mod_with('ProjectFeature')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index 02051310af7..d993db860c3 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -45,4 +45,4 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
-ProjectFeatureUsage.prepend_if_ee('EE::ProjectFeatureUsage')
+ProjectFeatureUsage.prepend_mod_with('ProjectFeatureUsage')
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index f065246e8af..d704f4c2c87 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -2,6 +2,7 @@
class ProjectGroupLink < ApplicationRecord
include Expirable
+ include EachBatch
belongs_to :project
belongs_to :group
@@ -49,4 +50,4 @@ class ProjectGroupLink < ApplicationRecord
end
end
-ProjectGroupLink.prepend_if_ee('EE::ProjectGroupLink')
+ProjectGroupLink.prepend_mod_with('ProjectGroupLink')
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 87ac6d38787..d374ee120d1 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -3,7 +3,7 @@
require 'carrierwave/orm/activerecord'
class ProjectImportData < ApplicationRecord
- prepend_if_ee('::EE::ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule
belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index 4bd3ffbea2f..633e669b5fc 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -105,4 +105,4 @@ class ProjectImportState < ApplicationRecord
end
end
-ProjectImportState.prepend_if_ee('EE::ProjectImportState')
+ProjectImportState.prepend_mod_with('ProjectImportState')
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
deleted file mode 100644
index f31bf931a41..00000000000
--- a/app/models/project_services/asana_service.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-require 'asana'
-
-class AsanaService < Service
- include ActionView::Helpers::UrlHelper
-
- prop_accessor :api_key, :restrict_to_branch
- validates :api_key, presence: true, if: :activated?
-
- def title
- 'Asana'
- end
-
- def description
- s_('AsanaService|Add commit messages as comments to Asana tasks')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
- s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'asana'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'api_key',
- title: 'API key',
- help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
- # Example Personal Access Token from Asana docs
- placeholder: '0/68a9e79b868c6789e79a124c30b0',
- required: true
- },
- {
- type: 'text',
- name: 'restrict_to_branch',
- title: 'Restrict to branch (optional)',
- help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
- }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def client
- @_client ||= begin
- Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
- end
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- # check the branch restriction is poplulated and branch is not included
- branch = Gitlab::Git.ref_name(data[:ref])
- branch_restriction = restrict_to_branch.to_s
- if branch_restriction.present? && branch_restriction.index(branch).nil?
- return
- end
-
- user = data[:user_name]
- project_name = project.full_name
-
- data[:commits].each do |commit|
- push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
- check_commit(commit[:message], push_msg)
- end
- end
-
- def check_commit(message, push_msg)
- # matches either:
- # - #1234
- # - https://app.asana.com/0/{project_gid}/{task_gid}
- # optionally preceded with:
- # - fix/ed/es/ing
- # - close/s/d
- # - closing
- issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
-
- message.scan(issue_finder).each do |tuple|
- # tuple will be
- # [ 'fix', 'id_from_url', 'id_from_pound' ]
- taskid = tuple[2] || tuple[1]
-
- begin
- task = Asana::Resources::Task.find_by_id(client, taskid)
- task.add_comment(text: "#{push_msg} #{message}")
-
- if tuple[0]
- task.update(completed: true)
- end
- rescue => e
- log_error(e.message)
- next
- end
- end
- end
-end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
deleted file mode 100644
index 8845fb99605..00000000000
--- a/app/models/project_services/assembla_service.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-class AssemblaService < Service
- prop_accessor :token, :subdomain
- validates :token, presence: true, if: :activated?
-
- def title
- 'Assembla'
- end
-
- def description
- _('Manage projects.')
- end
-
- def self.to_param
- 'assembla'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'subdomain', placeholder: '' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
- end
-end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
deleted file mode 100644
index a892d1a4314..00000000000
--- a/app/models/project_services/bamboo_service.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-# frozen_string_literal: true
-
-class BambooService < CiService
- include ActionView::Helpers::UrlHelper
- include ReactiveService
-
- prop_accessor :bamboo_url, :build_key, :username, :password
-
- validates :bamboo_url, presence: true, public_url: true, if: :activated?
- validates :build_key, presence: true, if: :activated?
- validates :username,
- presence: true,
- if: ->(service) { service.activated? && service.password }
- validates :password,
- presence: true,
- if: ->(service) { service.activated? && service.username }
-
- attr_accessor :response
-
- after_save :compose_service_hook, if: :activated?
- before_update :reset_password
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.save
- end
-
- def reset_password
- if bamboo_url_changed? && !password_touched?
- self.password = nil
- end
- end
-
- def title
- s_('BambooService|Atlassian Bamboo')
- end
-
- def description
- s_('BambooService|Use the Atlassian Bamboo CI/CD server with GitLab.')
- end
-
- def help
- docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
- s_('BambooService|Use Atlassian Bamboo to run CI/CD pipelines. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
- end
-
- def self.to_param
- 'bamboo'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'bamboo_url',
- title: s_('BambooService|Bamboo URL'),
- placeholder: s_('https://bamboo.example.com'),
- help: s_('BambooService|Bamboo service root URL.'),
- required: true
- },
- {
- type: 'text',
- name: 'build_key',
- placeholder: s_('KEY'),
- help: s_('BambooService|Bamboo build plan key.'),
- required: true
- },
- {
- type: 'text',
- name: 'username',
- help: s_('BambooService|The user with API access to the Bamboo server.')
- },
- {
- type: 'password',
- name: 'password',
- non_empty_password_title: s_('ProjectService|Enter new password'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
- }
- ]
- end
-
- def build_page(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
- end
-
- def commit_status(sha, ref)
- with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action", { buildKey: build_key })
- end
-
- def calculate_reactive_cache(sha, ref)
- response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
-
- { build_page: read_build_page(response), commit_status: read_commit_status(response) }
- end
-
- private
-
- def get_build_result(response)
- return if response&.code != 200
-
- # May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
- result = response.dig('results', 'results', 'result')
-
- # In case of multiple results, arbitrarily assume the last one is the most relevant.
- return result.last if result.is_a?(Array)
-
- result
- end
-
- def read_build_page(response)
- result = get_build_result(response)
- key =
- if result.blank?
- # If actual build link can't be determined, send user to build summary page.
- build_key
- else
- # If actual build link is available, go to build result page.
- result.dig('planResultKey', 'key')
- end
-
- build_url("browse/#{key}")
- end
-
- def read_commit_status(response)
- return :error unless response && (response.code == 200 || response.code == 404)
-
- result = get_build_result(response)
- status =
- if result.blank?
- 'Pending'
- else
- result.dig('buildState')
- end
-
- return :error unless status.present?
-
- if status.include?('Success')
- 'success'
- elsif status.include?('Failed')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
- end
-
- def try_get_path(path, query_params = {})
- params = build_get_params(query_params)
- params[:extra_log_info] = { project_id: project_id }
-
- Gitlab::HTTP.try_get(build_url(path), params)
- end
-
- def get_path(path, query_params = {})
- Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
- end
-
- def build_url(path)
- Gitlab::Utils.append_path(bamboo_url, path)
- end
-
- def build_get_params(query_params)
- params = { verify: false, query: query_params }
- return params if username.blank? && password.blank?
-
- query_params[:os_authType] = 'basic'
- params[:basic_auth] = basic_auth
- params
- end
-
- def basic_auth
- { username: username, password: password }
- end
-end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 4332db3e961..d1c56d2a4d5 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class BugzillaService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def title
@@ -8,7 +10,12 @@ class BugzillaService < IssueTrackerService
end
def description
- s_('IssueTracker|Bugzilla issue tracker')
+ s_("IssueTracker|Use Bugzilla as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 53bb7b47b41..f2ea5066e37 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -68,7 +68,7 @@ class BuildkiteService < CiService
end
def description
- 'Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines on your own infrastructure'
+ 'Run CI/CD pipelines with Buildkite.'
end
def self.to_param
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
deleted file mode 100644
index f2295a95b60..00000000000
--- a/app/models/project_services/builds_email_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-# This class is to be removed with 9.1
-# We should also by then remove BuildsEmailService from database
-class BuildsEmailService < Service
- def self.to_param
- 'builds_email'
- end
-
- def self.supported_events
- %w[]
- end
-end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
deleted file mode 100644
index ad26e42a21b..00000000000
--- a/app/models/project_services/campfire_service.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-class CampfireService < Service
- prop_accessor :token, :subdomain, :room
- validates :token, presence: true, if: :activated?
-
- def title
- 'Campfire'
- end
-
- def description
- 'Simple web-based real-time group chat'
- end
-
- def self.to_param
- 'campfire'
- end
-
- def fields
- [
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'subdomain', placeholder: '' },
- { type: 'text', name: 'room', placeholder: '' }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- message = build_message(data)
- speak(self.room, message, auth)
- end
-
- private
-
- def base_uri
- @base_uri ||= "https://#{subdomain}.campfirenow.com"
- end
-
- def auth
- # use a dummy password, as explained in the Campfire API doc:
- # https://github.com/basecamp/campfire-api#authentication
- @auth ||= {
- basic_auth: {
- username: token,
- password: 'X'
- }
- }
- end
-
- # Post a message into a room, returns the message Hash in case of success.
- # Returns nil otherwise.
- # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
- def speak(room_name, message, auth)
- room = rooms(auth).find { |r| r["name"] == room_name }
- return unless room
-
- path = "/room/#{room["id"]}/speak.json"
- body = {
- body: {
- message: {
- type: 'TextMessage',
- body: message
- }
- }
- }
- res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
- res.code == 201 ? res : nil
- end
-
- # Returns a list of rooms, or [].
- # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
- def rooms(auth)
- res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
- res.code == 200 ? res["rooms"] : []
- end
-
- def build_message(push)
- ref = Gitlab::Git.ref_name(push[:ref])
- before = push[:before]
- after = push[:after]
-
- message = []
- message << "[#{project.full_name}] "
- message << "#{push[:user_name]} "
-
- if Gitlab::Git.blank_ref?(before)
- message << "pushed new branch #{ref} \n"
- elsif Gitlab::Git.blank_ref?(after)
- message << "removed branch #{ref} \n"
- else
- message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
- message << "#{project.web_url}/compare/#{before}...#{after}"
- end
-
- message.join
- end
-end
diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb
deleted file mode 100644
index c8913775843..00000000000
--- a/app/models/project_services/chat_message/alert_message.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class AlertMessage < BaseMessage
- attr_reader :title
- attr_reader :alert_url
- attr_reader :severity
- attr_reader :events
- attr_reader :status
- attr_reader :started_at
-
- def initialize(params)
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @title = params.dig(:object_attributes, :title)
- @alert_url = params.dig(:object_attributes, :url)
- @severity = params.dig(:object_attributes, :severity)
- @events = params.dig(:object_attributes, :events)
- @status = params.dig(:object_attributes, :status)
- @started_at = params.dig(:object_attributes, :started_at)
- end
-
- def attachments
- [{
- title: title,
- title_link: alert_url,
- color: attachment_color,
- fields: attachment_fields
- }]
- end
-
- def message
- "Alert firing in #{project_name}"
- end
-
- private
-
- def attachment_color
- "#C95823"
- end
-
- def attachment_fields
- [
- {
- title: "Severity",
- value: severity.to_s.humanize,
- short: true
- },
- {
- title: "Events",
- value: events,
- short: true
- },
- {
- title: "Status",
- value: status.to_s.humanize,
- short: true
- },
- {
- title: "Start time",
- value: format_time(started_at),
- short: true
- }
- ]
- end
-
- # This formats time into the following format
- # April 23rd, 2020 1:06AM UTC
- def format_time(time)
- time = Time.zone.parse(time.to_s)
- time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
- end
- end
-end
diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb
deleted file mode 100644
index bdd77a919e3..00000000000
--- a/app/models/project_services/chat_message/base_message.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class BaseMessage
- RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/uploads\/[^\)]*)\)/.freeze
-
- attr_reader :markdown
- attr_reader :user_full_name
- attr_reader :user_name
- attr_reader :user_avatar
- attr_reader :project_name
- attr_reader :project_url
-
- def initialize(params)
- @markdown = params[:markdown] || false
- @project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
- @project_url = params.dig(:project, :web_url) || params[:project_url]
- @user_full_name = params.dig(:user, :name) || params[:user_full_name]
- @user_name = params.dig(:user, :username) || params[:user_name]
- @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
- end
-
- def user_combined_name
- if user_full_name.present?
- "#{user_full_name} (#{user_name})"
- else
- user_name
- end
- end
-
- def summary
- return message if markdown
-
- format(message)
- end
-
- def pretext
- summary
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- raise NotImplementedError
- end
-
- def activity
- raise NotImplementedError
- end
-
- private
-
- def message
- raise NotImplementedError
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
- end
-
- def format_relative_links(string)
- string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
- end
-
- def attachment_color
- '#345'
- end
-
- def link(text, url)
- "[#{text}](#{url})"
- end
-
- def pretty_duration(seconds)
- parse_string =
- if duration < 1.hour
- '%M:%S'
- else
- '%H:%M:%S'
- end
-
- Time.at(seconds).utc.strftime(parse_string)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
deleted file mode 100644
index 5deb757e60f..00000000000
--- a/app/models/project_services/chat_message/deployment_message.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class DeploymentMessage < BaseMessage
- attr_reader :commit_title
- attr_reader :commit_url
- attr_reader :deployable_id
- attr_reader :deployable_url
- attr_reader :environment
- attr_reader :short_sha
- attr_reader :status
- attr_reader :user_url
-
- def initialize(data)
- super
-
- @commit_title = data[:commit_title]
- @commit_url = data[:commit_url]
- @deployable_id = data[:deployable_id]
- @deployable_url = data[:deployable_url]
- @environment = data[:environment]
- @short_sha = data[:short_sha]
- @status = data[:status]
- @user_url = data[:user_url]
- end
-
- def attachments
- [{
- text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
- color: color
- }]
- end
-
- def activity
- {}
- end
-
- private
-
- def message
- if running?
- "Starting deploy to #{environment}"
- else
- "Deploy to #{environment} #{humanized_status}"
- end
- end
-
- def color
- case status
- when 'success'
- 'good'
- when 'canceled'
- 'warning'
- when 'failed'
- 'danger'
- else
- '#334455'
- end
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def deployment_link
- link("##{deployable_id}", deployable_url)
- end
-
- def user_link
- link(user_combined_name, user_url)
- end
-
- def commit_link
- link(short_sha, commit_url)
- end
-
- def humanized_status
- status == 'success' ? 'succeeded' : status
- end
-
- def running?
- status == 'running'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
deleted file mode 100644
index c8e90b66bae..00000000000
--- a/app/models/project_services/chat_message/issue_message.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class IssueMessage < BaseMessage
- attr_reader :title
- attr_reader :issue_iid
- attr_reader :issue_url
- attr_reader :action
- attr_reader :state
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @issue_iid = obj_attr[:iid]
- @issue_url = obj_attr[:url]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @description = obj_attr[:description] || ''
- end
-
- def attachments
- return [] unless opened_issue?
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "Issue #{state} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: issue_link,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
- end
-
- def opened_issue?
- action == 'open'
- end
-
- def description_message
- [{
- title: issue_title,
- title_link: issue_url,
- text: format(description),
- color: '#C95823'
- }]
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def issue_link
- link(issue_title, issue_url)
- end
-
- def issue_title
- "#{Issue.reference_prefix}#{issue_iid} #{title}"
- end
- end
-end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
deleted file mode 100644
index e45bb9b8ce1..00000000000
--- a/app/models/project_services/chat_message/merge_message.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class MergeMessage < BaseMessage
- attr_reader :merge_request_iid
- attr_reader :source_branch
- attr_reader :target_branch
- attr_reader :action
- attr_reader :state
- attr_reader :title
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @merge_request_iid = obj_attr[:iid]
- @source_branch = obj_attr[:source_branch]
- @target_branch = obj_attr[:target_branch]
- @action = obj_attr[:action]
- @state = obj_attr[:state]
- @title = format_title(obj_attr[:title])
- end
-
- def attachments
- []
- end
-
- def activity
- {
- title: "Merge request #{state_or_action_text} by #{user_combined_name}",
- subtitle: "in #{project_link}",
- text: merge_request_link,
- image: user_avatar
- }
- end
-
- private
-
- def format_title(title)
- '*' + title.lines.first.chomp + '*'
- end
-
- def message
- merge_request_message
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def merge_request_message
- "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
- end
-
- def merge_request_link
- link(merge_request_title, merge_request_url)
- end
-
- def merge_request_title
- "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
- end
-
- def merge_request_url
- "#{project_url}/-/merge_requests/#{merge_request_iid}"
- end
-
- def state_or_action_text
- case action
- when 'approved', 'unapproved'
- action
- when 'approval'
- 'added their approval to'
- when 'unapproval'
- 'removed their approval from'
- else
- state
- end
- end
- end
-end
diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb
deleted file mode 100644
index 741474fb27b..00000000000
--- a/app/models/project_services/chat_message/note_message.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class NoteMessage < BaseMessage
- attr_reader :note
- attr_reader :note_url
- attr_reader :title
- attr_reader :target
-
- def initialize(params)
- super
-
- params = HashWithIndifferentAccess.new(params)
- obj_attr = params[:object_attributes]
- @note = obj_attr[:note]
- @note_url = obj_attr[:url]
- @target, @title = case obj_attr[:noteable_type]
- when "Commit"
- create_commit_note(params[:commit])
- when "Issue"
- create_issue_note(params[:issue])
- when "MergeRequest"
- create_merge_note(params[:merge_request])
- when "Snippet"
- create_snippet_note(params[:snippet])
- end
- end
-
- def attachments
- return note if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
- subtitle: "in #{project_link}",
- text: formatted_title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
- end
-
- def format_title(title)
- title.lines.first.chomp
- end
-
- def formatted_title
- format_title(title)
- end
-
- def create_issue_note(issue)
- ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
- end
-
- def create_commit_note(commit)
- commit_sha = Commit.truncate_sha(commit[:id])
-
- ["commit #{commit_sha}", commit[:message]]
- end
-
- def create_merge_note(merge_request)
- ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
- end
-
- def create_snippet_note(snippet)
- ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
- end
-
- def description_message
- [{ text: format(note), color: attachment_color }]
- end
-
- def project_link
- link(project_name, project_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
deleted file mode 100644
index f4c6938fa78..00000000000
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ /dev/null
@@ -1,265 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PipelineMessage < BaseMessage
- MAX_VISIBLE_JOBS = 10
-
- attr_reader :user
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :detailed_status
- attr_reader :duration
- attr_reader :finished_at
- attr_reader :pipeline_id
- attr_reader :failed_stages
- attr_reader :failed_jobs
-
- attr_reader :project
- attr_reader :commit
- attr_reader :committer
- attr_reader :pipeline
-
- def initialize(data)
- super
-
- @user = data[:user]
- @user_name = data.dig(:user, :username) || 'API'
-
- pipeline_attributes = data[:object_attributes]
- @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
- @ref = pipeline_attributes[:ref]
- @status = pipeline_attributes[:status]
- @detailed_status = pipeline_attributes[:detailed_status]
- @duration = pipeline_attributes[:duration].to_i
- @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
- @pipeline_id = pipeline_attributes[:id]
-
- # Get list of jobs that have actually failed (after exhausting all retries)
- @failed_jobs = actually_failed_jobs(Array(data[:builds]))
- @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
-
- @project = Project.find(data[:project][:id])
- @commit = project.commit_by(oid: data[:commit][:id])
- @committer = commit.committer
- @pipeline = Ci::Pipeline.find(pipeline_id)
- end
-
- def pretext
- ''
- end
-
- def attachments
- return message if markdown
-
- [{
- fallback: format(message),
- color: attachment_color,
- author_name: user_combined_name,
- author_icon: user_avatar,
- author_link: author_url,
- title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
- {
- pipeline_id: pipeline_id,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- },
- title_link: pipeline_url,
- fields: attachments_fields,
- footer: project.name,
- footer_icon: project.avatar_url(only_path: false),
- ts: finished_at
- }]
- end
-
- def activity
- {
- title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
- {
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status
- },
- subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
- text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
- image: user_avatar || ''
- }
- end
-
- private
-
- def actually_failed_jobs(builds)
- succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
-
- failed_jobs = builds.select do |build|
- # Select jobs which doesn't have a successful retry
- build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
- end
-
- failed_jobs.uniq { |job| job[:name] }.reverse
- end
-
- def failed_stages_field
- {
- title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
- short: true
- }
- end
-
- def failed_jobs_field
- {
- title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
- value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
- short: true
- }
- end
-
- def yaml_error_field
- {
- title: s_("ChatMessage|Invalid CI config YAML file"),
- value: pipeline.yaml_errors,
- short: false
- }
- end
-
- def attachments_fields
- fields = [
- {
- title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
- short: true
- },
- {
- title: s_("ChatMessage|Commit"),
- value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
- short: true
- }
- ]
-
- fields << failed_stages_field if failed_stages.any?
- fields << failed_jobs_field if failed_jobs.any?
- fields << yaml_error_field if pipeline.has_yaml_errors?
-
- fields
- end
-
- def message
- s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
- {
- project_link: project_link,
- pipeline_link: pipeline_link,
- ref_type: ref_type,
- ref_link: ref_link,
- user_combined_name: user_combined_name,
- humanized_status: humanized_status,
- duration: pretty_duration(duration)
- }
- end
-
- def humanized_status
- case status
- when 'success'
- detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
- when 'failed'
- s_("ChatMessage|has failed")
- else
- status
- end
- end
-
- def attachment_color
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/-/commits/#{ref}"
- end
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "[#{project.name}](#{project_url})"
- end
-
- def pipeline_failed_jobs_url
- "#{project_url}/-/pipelines/#{pipeline_id}/failures"
- end
-
- def pipeline_url
- if failed_jobs.any?
- pipeline_failed_jobs_url
- else
- "#{project_url}/-/pipelines/#{pipeline_id}"
- end
- end
-
- def pipeline_link
- "[##{pipeline_id}](#{pipeline_url})"
- end
-
- def job_url(job)
- "#{project_url}/-/jobs/#{job[:id]}"
- end
-
- def job_link(job)
- "[#{job[:name]}](#{job_url(job)})"
- end
-
- def failed_jobs_links
- failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
- truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
-
- failed_links = failed.map { |job| job_link(job) }
-
- unless truncated.blank?
- failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
- count: truncated.size,
- pipeline_failed_jobs_url: pipeline_failed_jobs_url
- }
- end
-
- failed_links.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def stage_link(stage)
- # All stages link to the pipeline page
- "[#{stage}](#{pipeline_url})"
- end
-
- def failed_stages_links
- failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector'))
- end
-
- def commit_url
- Gitlab::UrlBuilder.build(commit)
- end
-
- def commit_link
- "[#{commit.title}](#{commit_url})"
- end
-
- def author_url
- return unless user && committer
-
- Gitlab::UrlBuilder.build(committer)
- end
- end
-end
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
deleted file mode 100644
index c8e70a69c88..00000000000
--- a/app/models/project_services/chat_message/push_message.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class PushMessage < BaseMessage
- attr_reader :after
- attr_reader :before
- attr_reader :commits
- attr_reader :ref
- attr_reader :ref_type
-
- def initialize(params)
- super
-
- @after = params[:after]
- @before = params[:before]
- @commits = params.fetch(:commits, [])
- @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
- @ref = Gitlab::Git.ref_name(params[:ref])
- end
-
- def attachments
- return [] if new_branch? || removed_branch?
- return commit_messages if markdown
-
- commit_message_attachments
- end
-
- def activity
- {
- title: humanized_action(short: true),
- subtitle: "in #{project_link}",
- text: compare_link,
- image: user_avatar
- }
- end
-
- private
-
- def humanized_action(short: false)
- action, ref_link, target_link = compose_action_details
- text = [user_combined_name, action, ref_type, ref_link]
- text << target_link unless short
- text.join(' ')
- end
-
- def message
- humanized_action
- end
-
- def format(string)
- Slack::Messenger::Util::LinkFormatter.format(string)
- end
-
- def commit_messages
- commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
- end
-
- def commit_message_attachments
- [{ text: format(commit_messages), color: attachment_color }]
- end
-
- def compose_commit_message(commit)
- author = commit[:author][:name]
- id = Commit.truncate_sha(commit[:id])
- title = commit[:title]
-
- url = commit[:url]
-
- "[#{id}](#{url}): #{title} - #{author}"
- end
-
- def new_branch?
- Gitlab::Git.blank_ref?(before)
- end
-
- def removed_branch?
- Gitlab::Git.blank_ref?(after)
- end
-
- def ref_url
- if ref_type == 'tag'
- "#{project_url}/-/tags/#{ref}"
- else
- "#{project_url}/commits/#{ref}"
- end
- end
-
- def compare_url
- "#{project_url}/compare/#{before}...#{after}"
- end
-
- def ref_link
- "[#{ref}](#{ref_url})"
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def compare_link
- "[Compare changes](#{compare_url})"
- end
-
- def compose_action_details
- if new_branch?
- ['pushed new', ref_link, "to #{project_link}"]
- elsif removed_branch?
- ['removed', ref, "from #{project_link}"]
- else
- ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
- end
- end
-
- def attachment_color
- '#345'
- end
- end
-end
diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
deleted file mode 100644
index ebe7abb379f..00000000000
--- a/app/models/project_services/chat_message/wiki_page_message.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-module ChatMessage
- class WikiPageMessage < BaseMessage
- attr_reader :title
- attr_reader :wiki_page_url
- attr_reader :action
- attr_reader :description
-
- def initialize(params)
- super
-
- obj_attr = params[:object_attributes]
- obj_attr = HashWithIndifferentAccess.new(obj_attr)
- @title = obj_attr[:title]
- @wiki_page_url = obj_attr[:url]
- @description = obj_attr[:message]
-
- @action =
- case obj_attr[:action]
- when "create"
- "created"
- when "update"
- "edited"
- end
- end
-
- def attachments
- return description if markdown
-
- description_message
- end
-
- def activity
- {
- title: "#{user_combined_name} #{action} #{wiki_page_link}",
- subtitle: "in #{project_link}",
- text: title,
- image: user_avatar
- }
- end
-
- private
-
- def message
- "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
- end
-
- def description_message
- [{ text: format(@description), color: attachment_color }]
- end
-
- def project_link
- "[#{project_name}](#{project_url})"
- end
-
- def wiki_page_link
- "[wiki page](#{wiki_page_url})"
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 4a99842b4d5..2f841bf903e 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -2,7 +2,7 @@
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
-class ChatNotificationService < Service
+class ChatNotificationService < Integration
include ChatMessage
include NotificationBranchSelection
@@ -15,9 +15,14 @@ class ChatNotificationService < Service
EVENT_CHANNEL = proc { |event| "#{event}_channel" }
+ LABEL_NOTIFICATION_BEHAVIOURS = [
+ MATCH_ANY_LABEL = 'match_any',
+ MATCH_ALL_LABELS = 'match_all'
+ ].freeze
+
default_value_for :category, 'chat'
- prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified
+ prop_accessor :webhook, :username, :channel, :branches_to_be_notified, :labels_to_be_notified, :labels_to_be_notified_behavior
# Custom serialized properties initialization
prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })
@@ -25,12 +30,14 @@ class ChatNotificationService < Service
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, public_url: true, if: :activated?
+ validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
if properties.nil?
self.properties = {}
self.notify_only_broken_pipelines = true
self.branches_to_be_notified = "default"
+ self.labels_to_be_notified_behavior = MATCH_ANY_LABEL
elsif !self.notify_only_default_branch.nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
@@ -65,7 +72,20 @@ class ChatNotificationService < Service
{ type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }.freeze,
- { type: 'text', name: 'labels_to_be_notified', placeholder: '~backend,~frontend', help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.' }.freeze
+ {
+ type: 'text',
+ name: 'labels_to_be_notified',
+ placeholder: '~backend,~frontend',
+ help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
+ }.freeze,
+ {
+ type: 'select',
+ name: 'labels_to_be_notified_behavior',
+ choices: [
+ ['Match any of the labels', MATCH_ANY_LABEL],
+ ['Match all of the labels', MATCH_ALL_LABELS]
+ ]
+ }.freeze
].freeze
end
@@ -136,11 +156,17 @@ class ChatNotificationService < Service
def notify_label?(data)
return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER.include?(data[:object_kind]) && labels_to_be_notified.present?
- issue_labels = data.dig(:issue, :labels) || []
- merge_request_labels = data.dig(:merge_request, :labels) || []
- label_titles = (issue_labels + merge_request_labels).pluck(:title)
+ labels = data.dig(:issue, :labels) || data.dig(:merge_request, :labels)
+
+ return false if labels.nil?
- (labels_to_be_notified_list & label_titles).any?
+ matching_labels = labels_to_be_notified_list & labels.pluck(:title)
+
+ if labels_to_be_notified_behavior == MATCH_ALL_LABELS
+ labels_to_be_notified_list.difference(matching_labels).empty?
+ else
+ matching_labels.any?
+ end
end
def user_id_from_hook_data(data)
@@ -159,19 +185,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
+ Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
- ChatMessage::IssueMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
- ChatMessage::MergeMessage.new(data) unless update?(data)
+ Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
- ChatMessage::NoteMessage.new(data)
+ Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline"
- ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
- ChatMessage::WikiPageMessage.new(data)
+ Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
- ChatMessage::DeploymentMessage.new(data)
+ Integrations::ChatMessage::DeploymentMessage.new(data)
end
end
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 29edb9ec16f..0733da761d5 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -3,7 +3,7 @@
# Base class for CI services
# List methods you need to implement to get your CI service
# working with GitLab merge requests
-class CiService < Service
+class CiService < Integration
default_value_for :category, 'ci'
def valid_token?(token)
diff --git a/app/models/project_services/confluence_service.rb b/app/models/project_services/confluence_service.rb
deleted file mode 100644
index 8a6f4de540c..00000000000
--- a/app/models/project_services/confluence_service.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-class ConfluenceService < Service
- include ActionView::Helpers::UrlHelper
-
- VALID_SCHEME_MATCH = %r{\Ahttps?\Z}.freeze
- VALID_HOST_MATCH = %r{\A.+\.atlassian\.net\Z}.freeze
- VALID_PATH_MATCH = %r{\A/wiki(/|\Z)}.freeze
-
- prop_accessor :confluence_url
-
- validates :confluence_url, presence: true, if: :activated?
- validate :validate_confluence_url_is_cloud, if: :activated?
-
- after_commit :cache_project_has_confluence
-
- def self.to_param
- 'confluence'
- end
-
- def self.supported_events
- %w()
- end
-
- def title
- s_('ConfluenceService|Confluence Workspace')
- end
-
- def description
- s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
- end
-
- def help
- return unless project&.wiki_enabled?
-
- if activated?
- wiki_url = project.wiki.web_url
-
- s_(
- 'ConfluenceService|Your GitLab Wiki can be accessed here: %{wiki_link}. To re-enable your GitLab Wiki, disable this integration' %
- { wiki_link: link_to(wiki_url, wiki_url) }
- ).html_safe
- else
- s_('ConfluenceService|Enabling the Confluence Workspace will disable the default GitLab Wiki. Your GitLab Wiki data will be saved and you can always re-enable it later by turning off this integration').html_safe
- end
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'confluence_url',
- title: 'Confluence Cloud Workspace URL',
- placeholder: s_('ConfluenceService|The URL of the Confluence Workspace'),
- required: true
- }
- ]
- end
-
- def can_test?
- false
- end
-
- private
-
- def validate_confluence_url_is_cloud
- unless confluence_uri_valid?
- errors.add(:confluence_url, 'URL must be to a Confluence Cloud Workspace hosted on atlassian.net')
- end
- end
-
- def confluence_uri_valid?
- return false unless confluence_url
-
- uri = URI.parse(confluence_url)
-
- (uri.scheme&.match(VALID_SCHEME_MATCH) &&
- uri.host&.match(VALID_HOST_MATCH) &&
- uri.path&.match(VALID_PATH_MATCH)).present?
-
- rescue URI::InvalidURIError
- false
- end
-
- def cache_project_has_confluence
- return unless project && !project.destroyed?
-
- project.project_setting.save! unless project.project_setting.persisted?
- project.project_setting.update_column(:has_confluence, active?)
- end
-end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index aab8661ec55..6f99d104904 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,25 +1,23 @@
# frozen_string_literal: true
class CustomIssueTrackerService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def title
- 'Custom Issue Tracker'
+ s_('IssueTracker|Custom issue tracker')
end
def description
- s_('IssueTracker|Custom issue tracker')
+ s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
end
- def self.to_param
- 'custom_issue_tracker'
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer'
+ s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
- def fields
- [
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
- { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
- ]
+ def self.to_param
+ 'custom_issue_tracker'
end
end
diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
index 12ebf260e08..ca4dc0375fb 100644
--- a/app/models/project_services/data_fields.rb
+++ b/app/models/project_services/data_fields.rb
@@ -42,9 +42,9 @@ module DataFields
end
included do
- has_one :issue_tracker_data, autosave: true
- has_one :jira_tracker_data, autosave: true
- has_one :open_project_tracker_data, autosave: true
+ has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
+ has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
+ has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id
def data_fields
raise NotImplementedError
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
deleted file mode 100644
index 9a2d99c46c9..00000000000
--- a/app/models/project_services/datadog_service.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-# frozen_string_literal: true
-
-class DatadogService < Service
- DEFAULT_SITE = 'datadoghq.com'
- URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'
- URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'
- URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/"
-
- SUPPORTED_EVENTS = %w[
- pipeline job
- ].freeze
-
- prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env
-
- with_options if: :activated? do
- validates :api_key, presence: true, format: { with: /\A\w+\z/ }
- validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true }
- validates :api_url, public_url: { allow_blank: true }
- validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? }
- validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? }
- end
-
- after_save :compose_service_hook, if: :activated?
-
- def initialize_properties
- super
-
- self.datadog_site ||= DEFAULT_SITE
- end
-
- def self.supported_events
- SUPPORTED_EVENTS
- end
-
- def self.default_test_event
- 'pipeline'
- end
-
- def configurable_events
- [] # do not allow to opt out of required hooks
- end
-
- def title
- 'Datadog'
- end
-
- def description
- 'Trace your GitLab pipelines with Datadog'
- end
-
- def help
- nil
- # Maybe adding something in the future
- # We could link to static help pages as well
- # [More information](#{Gitlab::Routing.url_helpers.help_page_url('integration/datadog')})"
- end
-
- def self.to_param
- 'datadog'
- end
-
- def fields
- [
- {
- type: 'text',
- name: 'datadog_site',
- placeholder: DEFAULT_SITE,
- help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
- required: false
- },
- {
- type: 'text',
- name: 'api_url',
- title: 'API URL',
- help: '(Advanced) Define the full URL for your Datadog site directly',
- required: false
- },
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
- required: true
- },
- {
- type: 'text',
- name: 'datadog_service',
- title: 'Service',
- placeholder: 'gitlab-ci',
- help: 'Name of this GitLab instance that all data will be tagged with'
- },
- {
- type: 'text',
- name: 'datadog_env',
- title: 'Env',
- help: 'The environment tag that traces will be tagged with'
- }
- ]
- end
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.url = hook_url
- hook.save
- end
-
- def hook_url
- url = api_url.presence || sprintf(URL_TEMPLATE, datadog_site: datadog_site)
- url = URI.parse(url)
- url.path = File.join(url.path || '/', api_key)
- query = { service: datadog_service.presence, env: datadog_env.presence }.compact
- url.query = query.to_query unless query.empty?
- url.to_s
- end
-
- def api_keys_url
- return URL_API_KEYS_DOCS unless datadog_site.presence
-
- sprintf(URL_TEMPLATE_API_KEYS, datadog_site: datadog_site)
- end
-
- def execute(data)
- return if project.disabled_services.include?(to_param)
-
- object_kind = data[:object_kind]
- object_kind = 'job' if object_kind == 'build'
- return unless supported_events.include?(object_kind)
-
- service_hook.execute(data, "#{object_kind} hook")
- end
-
- def test(data)
- begin
- result = execute(data)
- return { success: false, result: result[:message] } if result[:http_status] != 200
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result[:message] }
- end
-end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
deleted file mode 100644
index cdb69684d16..00000000000
--- a/app/models/project_services/emails_on_push_service.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-class EmailsOnPushService < Service
- include NotificationBranchSelection
-
- RECIPIENTS_LIMIT = 750
-
- boolean_accessor :send_from_committer_email
- boolean_accessor :disable_diffs
- prop_accessor :recipients, :branches_to_be_notified
- validates :recipients, presence: true, if: :validate_recipients?
- validate :number_of_recipients_within_limit, if: :validate_recipients?
-
- def self.valid_recipients(recipients)
- recipients.split.select do |recipient|
- recipient.include?('@')
- end.uniq(&:downcase)
- end
-
- def title
- s_('EmailsOnPushService|Emails on push')
- end
-
- def description
- s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
- end
-
- def self.to_param
- 'emails_on_push'
- end
-
- def self.supported_events
- %w(push tag_push)
- end
-
- def initialize_properties
- super
-
- self.branches_to_be_notified = 'all' if branches_to_be_notified.nil?
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return if project.emails_disabled?
- return unless notify_for_ref?(push_data)
-
- EmailsOnPushWorker.perform_async(
- project_id,
- recipients,
- push_data,
- send_from_committer_email: send_from_committer_email?,
- disable_diffs: disable_diffs?
- )
- end
-
- def notify_for_ref?(push_data)
- return true if push_data[:object_kind] == 'tag_push'
- return true if push_data.dig(:object_attributes, :tag)
-
- notify_for_branch?(push_data)
- end
-
- def send_from_committer_email?
- Gitlab::Utils.to_boolean(self.send_from_committer_email)
- end
-
- def disable_diffs?
- Gitlab::Utils.to_boolean(self.disable_diffs)
- end
-
- def fields
- domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ")
- [
- { type: 'checkbox', name: 'send_from_committer_email', title: s_("EmailsOnPushService|Send from committer"),
- help: s_("EmailsOnPushService|Send notifications from the committer's email address if the domain matches the domain used by your GitLab instance (such as %{domains}).") % { domains: domains } },
- { type: 'checkbox', name: 'disable_diffs', title: s_("EmailsOnPushService|Disable code diffs"),
- help: s_("EmailsOnPushService|Don't include possibly sensitive code diffs in notification body.") },
- { type: 'select', name: 'branches_to_be_notified', choices: branch_choices },
- {
- type: 'textarea',
- name: 'recipients',
- placeholder: s_('EmailsOnPushService|tanuki@example.com gitlab@example.com'),
- help: s_('EmailsOnPushService|Emails separated by whitespace.')
- }
- ]
- end
-
- private
-
- def number_of_recipients_within_limit
- return if recipients.blank?
-
- if self.class.valid_recipients(recipients).size > RECIPIENTS_LIMIT
- errors.add(:recipients, s_("EmailsOnPushService|can't exceed %{recipients_limit}") % { recipients_limit: RECIPIENTS_LIMIT })
- end
- end
-end
diff --git a/app/models/project_services/ewm_service.rb b/app/models/project_services/ewm_service.rb
index af402e50292..90fcbb10d2b 100644
--- a/app/models/project_services/ewm_service.rb
+++ b/app/models/project_services/ewm_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class EwmService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
def self.reference_pattern(only_long: true)
@@ -12,7 +14,12 @@ class EwmService < IssueTrackerService
end
def description
- s_('IssueTracker|EWM work items tracker')
+ s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index c41783d1af4..f49b008533d 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
-class ExternalWikiService < Service
+class ExternalWikiService < Integration
include ActionView::Helpers::UrlHelper
+
prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
@@ -39,7 +40,7 @@ class ExternalWikiService < Service
def execute(_data)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response.body if response.code == 200
- rescue
+ rescue StandardError
nil
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index e721fded1d9..7aae5af7454 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class FlowdockService < Service
+class FlowdockService < Integration
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :token
validates :token, presence: true, if: :activated?
@@ -9,7 +11,12 @@ class FlowdockService < Service
end
def description
- s_('FlowdockService|Flowdock is a collaboration web app for technical teams.')
+ s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.')
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer'
+ s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -18,7 +25,7 @@ class FlowdockService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: s_('FlowdockService|Flowdock Git source token'), required: true }
+ { type: 'text', name: 'token', placeholder: s_('FlowdockService|1b609b52537...'), required: true, help: 'Enter your Flowdock token.' }
]
end
diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb
index 299a306add7..6e7708a169f 100644
--- a/app/models/project_services/hangouts_chat_service.rb
+++ b/app/models/project_services/hangouts_chat_service.rb
@@ -3,12 +3,14 @@
require 'hangouts_chat'
class HangoutsChatService < ChatNotificationService
+ include ActionView::Helpers::UrlHelper
+
def title
- 'Hangouts Chat'
+ 'Google Chat'
end
def description
- 'Receive event notifications in Google Hangouts Chat'
+ 'Send notifications from GitLab to a room in Google Chat.'
end
def self.to_param
@@ -16,13 +18,8 @@ class HangoutsChatService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to Google Hangouts Chat room.<br />
- To set up this service:
- <ol>
- <li><a href="https://developers.google.com/hangouts/chat/how-tos/webhooks">Set up an incoming webhook for your room</a>. All notifications will come to this room.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>'
+ docs_link = link_to _('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Before enabling this integration, create a webhook for the room in Google Chat where you want to receive notifications from this project. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
@@ -42,7 +39,7 @@ class HangoutsChatService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', placeholder: "#{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index cd49c6d253d..71d8e7bfac4 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -1,54 +1,17 @@
# frozen_string_literal: true
-class HipchatService < Service
- include ActionView::Helpers::SanitizeHelper
-
- MAX_COMMITS = 3
- HIPCHAT_ALLOWED_TAGS = %w[
- a b i strong em br img pre code
- table th tr td caption colgroup col thead tbody tfoot
- ul ol li dl dt dd
- ].freeze
-
- prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_pipelines, :notify
- validates :token, presence: true, if: :activated?
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_pipelines = true
- end
- end
-
- def title
- 'HipChat'
- end
-
- def description
- 'Private group chat and IM'
- end
+# This service is scheduled for removal. All records must
+# be deleted before the class can be removed.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/27954
+class HipchatService < Integration
+ before_save :prevent_save
def self.to_param
'hipchat'
end
- def fields
- [
- { type: 'text', name: 'token', placeholder: 'Room token', required: true },
- { type: 'text', name: 'room', placeholder: 'Room name or ID' },
- { type: 'checkbox', name: 'notify' },
- { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
- { type: 'text', name: 'api_version', title: _('API version'),
- placeholder: 'Leave blank for default (v2)' },
- { type: 'text', name: 'server',
- placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_pipelines' }
- ]
- end
-
def self.supported_events
- %w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
+ []
end
def execute(data)
@@ -56,96 +19,14 @@ class HipchatService < Service
# HipChat is unusable anyway, so do nothing in this method
end
- def test(data)
- begin
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
private
- def message_options(data = nil)
- { notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
- end
-
- def render_line(text)
- markdown(text.lines.first.chomp, pipeline: :single_line) if text
- end
-
- def markdown(text, options = {})
- return "" unless text
-
- context = {
- project: project,
- pipeline: :email
- }
-
- Banzai.render(text, context)
-
- context.merge!(options)
-
- html = Banzai.render_and_post_process(text, context)
- sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
-
- sanitized_html.truncate(200, separator: ' ', omission: '...')
- end
-
- def format_title(title)
- "<b>#{render_line(title)}</b>"
- end
-
- def message_color(data)
- pipeline_status_color(data) || color || 'yellow'
- end
-
- def pipeline_status_color(data)
- return unless data && data[:object_kind] == 'pipeline'
-
- case data[:object_attributes][:status]
- when 'success'
- 'green'
- else
- 'red'
- end
- end
-
- def project_name
- project.full_name.gsub(/\s/, '')
- end
-
- def project_url
- project.web_url
- end
-
- def project_link
- "<a href=\"#{project_url}\">#{project_name}</a>"
- end
-
- def update?(data)
- data[:object_attributes][:action] == 'update'
- end
-
- def humanized_status(status)
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
+ def prevent_save
+ errors.add(:base, _('HipChat endpoint is deprecated and should not be created or modified.'))
- def should_pipeline_be_notified?(data)
- case data[:object_attributes][:status]
- when 'success'
- !notify_only_broken_pipelines?
- when 'failed'
- true
- else
- false
- end
+ # Stops execution of callbacks and database operation while
+ # preserving expectations of #save (will not raise) & #save! (raises)
+ # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution
+ throw :abort # rubocop:disable Cop/BanCatchThrow
end
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 4f1ce16ebb2..5cca620c659 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -2,7 +2,7 @@
require 'uri'
-class IrkerService < Service
+class IrkerService < Integration
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
@@ -15,8 +15,7 @@ class IrkerService < Service
end
def description
- 'Send IRC messages, on update, to a list of recipients through an Irker '\
- 'gateway.'
+ 'Send IRC messages.'
end
def self.to_param
@@ -103,7 +102,7 @@ class IrkerService < Service
begin
new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
uri = consider_uri(URI.parse(new_recipient))
- rescue
+ rescue StandardError
log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
end
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 19a5b4a74bb..099e3c336dd 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class IssueTrackerService < Service
+class IssueTrackerService < Integration
validate :one_issue_tracker, if: :activated?, on: :manual_change
# TODO: we can probably just delegate as part of
@@ -73,9 +73,9 @@ class IssueTrackerService < Service
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
- { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in the external issue tracker.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('IssueTracker|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the external issue tracker. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('IssueTracker|New issue URL'), help: s_('IssueTracker|The URL to create an issue in the external issue tracker.'), required: true }
]
end
@@ -143,10 +143,10 @@ class IssueTrackerService < Service
return if template? || instance?
return if project.blank?
- if project.services.external_issue_trackers.where.not(id: id).any?
+ if project.integrations.external_issue_trackers.where.not(id: id).any?
errors.add(:base, _('Another issue tracker is already in use. Only one issue tracker service can be active at a time'))
end
end
end
-IssueTrackerService.prepend_if_ee('EE::IssueTrackerService')
+IssueTrackerService.prepend_mod_with('IssueTrackerService')
diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb
index 6a123517b84..990a35cd617 100644
--- a/app/models/project_services/jenkins_service.rb
+++ b/app/models/project_services/jenkins_service.rb
@@ -64,12 +64,12 @@ class JenkinsService < CiService
end
def description
- s_('An extendable open source CI/CD server.')
+ s_('Run CI/CD pipelines with Jenkins.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
- s_('Trigger Jenkins builds when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 3e14bf44c12..5cd6e79eb1d 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -106,9 +106,8 @@ class JiraService < IssueTrackerService
end
def help
- "You need to configure Jira before enabling this service. For more details
- read the
- [Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
+ jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
+ s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end
def title
@@ -116,7 +115,7 @@ class JiraService < IssueTrackerService
end
def description
- s_('JiraService|Track issues in Jira')
+ s_("JiraService|Use Jira as this project's issue tracker.")
end
def self.to_param
@@ -305,7 +304,7 @@ class JiraService < IssueTrackerService
)
true
- rescue => error
+ rescue StandardError => error
log_error(
"Issue transition failed",
error: {
@@ -490,7 +489,7 @@ class JiraService < IssueTrackerService
# Handle errors when doing Jira API calls
def jira_request
yield
- rescue => error
+ rescue StandardError => error
@error = error
log_error("Error sending message", client_url: client_url, error: @error.message)
nil
@@ -539,4 +538,4 @@ class JiraService < IssueTrackerService
end
end
-JiraService.prepend_if_ee('EE::JiraService')
+JiraService.prepend_mod_with('JiraService')
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index 803c1255195..1d2067067da 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -6,7 +6,7 @@ class MicrosoftTeamsService < ChatNotificationService
end
def description
- 'Receive event notifications in Microsoft Teams'
+ 'Send notifications about project events to Microsoft Teams.'
end
def self.to_param
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
index 1b530a8247b..ea65a200027 100644
--- a/app/models/project_services/monitoring_service.rb
+++ b/app/models/project_services/monitoring_service.rb
@@ -4,7 +4,7 @@
#
# These services integrate with a deployment solution like Prometheus
# to provide additional features for environments.
-class MonitoringService < Service
+class MonitoringService < Integration
default_value_for :category, 'monitoring'
def self.supported_events
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
index 21f0a2b2463..f3ea8c64302 100644
--- a/app/models/project_services/packagist_service.rb
+++ b/app/models/project_services/packagist_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PackagistService < Service
+class PackagistService < Integration
prop_accessor :username, :token, :server
validates :username, presence: true, if: :activated?
@@ -16,7 +16,7 @@ class PackagistService < Service
end
def description
- s_('Integrations|Update your projects on Packagist, the main Composer repository')
+ s_('Integrations|Update your Packagist projects.')
end
def self.to_param
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 0a0a41c525c..4603193ac8e 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PipelinesEmailService < Service
+class PipelinesEmailService < Integration
include NotificationBranchSelection
prop_accessor :recipients, :branches_to_be_notified
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index d3fff100964..6e67984591d 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PivotaltrackerService < Service
+class PivotaltrackerService < Integration
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
prop_accessor :token, :restrict_to_branch
@@ -11,7 +11,7 @@ class PivotaltrackerService < Service
end
def description
- s_('PivotalTrackerService|Project Management Software (Source Commits Endpoint)')
+ s_('PivotalTrackerService|Add commit messages as comments to PivotalTracker stories.')
end
def self.to_param
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 1781ec7456d..89765fbdf41 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class PushoverService < Service
+class PushoverService < Integration
BASE_URI = 'https://api.pushover.net/1'
prop_accessor :api_key, :user_key, :device, :priority, :sound
@@ -11,7 +11,7 @@ class PushoverService < Service
end
def description
- s_('PushoverService|Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.')
+ s_('PushoverService|Get real-time notifications on your device.')
end
def self.to_param
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index 26a6cf86bf4..7a0f500209c 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -9,7 +9,7 @@ class RedmineService < IssueTrackerService
end
def description
- s_('IssueTracker|Use Redmine as the issue tracker.')
+ s_("IssueTracker|Use Redmine as this project's issue tracker.")
end
def help
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 7badcc24870..92a46f8d01f 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -39,7 +39,7 @@ class SlackService < ChatNotificationService
end
def get_message(object_kind, data)
- return ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
+ return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index d436176a52c..37d16737052 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -2,7 +2,7 @@
# Base class for Chat services
# This class is not meant to be used directly, but only to inherrit from.
-class SlashCommandsService < Service
+class SlashCommandsService < Integration
default_value_for :category, 'chat'
prop_accessor :token
diff --git a/app/models/project_services/unify_circuit_service.rb b/app/models/project_services/unify_circuit_service.rb
index 1a0eebe7d64..5f43388e1c9 100644
--- a/app/models/project_services/unify_circuit_service.rb
+++ b/app/models/project_services/unify_circuit_service.rb
@@ -6,7 +6,7 @@ class UnifyCircuitService < ChatNotificationService
end
def description
- 'Receive event notifications in Unify Circuit'
+ s_('Integrations|Send notifications about project events to Unify Circuit.')
end
def self.to_param
diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb
index 4e8281f4e81..3d92d3bb85e 100644
--- a/app/models/project_services/webex_teams_service.rb
+++ b/app/models/project_services/webex_teams_service.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
class WebexTeamsService < ChatNotificationService
+ include ActionView::Helpers::UrlHelper
+
def title
- 'Webex Teams'
+ s_("WebexTeamsService|Webex Teams")
end
def description
- 'Receive event notifications in Webex Teams'
+ s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
def self.to_param
@@ -14,13 +16,8 @@ class WebexTeamsService < ChatNotificationService
end
def help
- 'This service sends notifications about projects events to a Webex Teams conversation.<br />
- To set up this service:
- <ol>
- <li><a href="https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
- <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
- <li>Select events below to enable notifications.</li>
- </ol>'
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
+ s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
def event_field(event)
@@ -36,7 +33,7 @@ class WebexTeamsService < ChatNotificationService
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. https://api.ciscospark.com/v1/webhooks/incoming/…", required: true },
+ { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 30abd0159b3..9760a22a872 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class YoutrackService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
@@ -17,7 +19,12 @@ class YoutrackService < IssueTrackerService
end
def description
- s_('IssueTracker|YouTrack issue tracker')
+ s_("IssueTracker|Use YouTrack as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -26,8 +33,8 @@ class YoutrackService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
- { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), help: s_('IssueTracker|The URL to the project in YouTrack.'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), help: s_('IssueTracker|The URL to view an issue in the YouTrack project. Must contain %{colon_id}.') % { colon_id: '<code>:id</code>'.html_safe }, required: true }
]
end
end
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 83ff0702b88..24d892290a6 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,4 +21,4 @@ class ProjectSetting < ApplicationRecord
end
end
-ProjectSetting.prepend_ee_mod
+ProjectSetting.prepend_mod
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 8c3dcaa7c0f..37ddd2d030d 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -159,4 +159,4 @@ class ProjectStatistics < ApplicationRecord
end
end
-ProjectStatistics.prepend_if_ee('EE::ProjectStatistics')
+ProjectStatistics.prepend_mod_with('ProjectStatistics')
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 1a3f362e6a1..a85afada901 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -130,7 +130,7 @@ class ProjectTeam
end
true
- rescue
+ rescue StandardError
false
end
@@ -234,4 +234,4 @@ class ProjectTeam
end
end
-ProjectTeam.prepend_if_ee('EE::ProjectTeam')
+ProjectTeam.prepend_mod_with('ProjectTeam')
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 91fb3d4e4ba..ffffa803011 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -32,4 +32,4 @@ end
# TODO: Remove this once we implement ES support for group wikis.
# https://gitlab.com/gitlab-org/gitlab/-/issues/207889
-ProjectWiki.prepend_if_ee('EE::ProjectWiki')
+ProjectWiki.prepend_mod_with('ProjectWiki')
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 963a6b7774a..889eaed138d 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -63,4 +63,4 @@ class ProtectedBranch < ApplicationRecord
end
end
-ProtectedBranch.prepend_if_ee('EE::ProtectedBranch')
+ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
index 2786ecb641a..8358be35470 100644
--- a/app/models/push_event_payload.rb
+++ b/app/models/push_event_payload.rb
@@ -25,4 +25,4 @@ class PushEventPayload < ApplicationRecord
}
end
-PushEventPayload.prepend_if_ee('EE::PushEventPayload')
+PushEventPayload.prepend_mod_with('PushEventPayload')
diff --git a/app/models/release.rb b/app/models/release.rb
index 5ca8f537baa..1889a0707b4 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -13,6 +13,7 @@ class Release < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :links, class_name: 'Releases::Link'
+ has_many :sorted_links, -> { sorted }, class_name: 'Releases::Link', inverse_of: :release
has_many :milestone_releases
has_many :milestones, through: :milestone_releases
@@ -23,11 +24,15 @@ class Release < ApplicationRecord
before_create :set_released_at
validates :project, :tag, presence: true
+ validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
scope :sorted, -> { order(released_at: :desc) }
- scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
+ scope :preloaded, -> {
+ includes(:author, :evidences, :milestones, :links, :sorted_links,
+ 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)) }
@@ -58,8 +63,8 @@ class Release < ApplicationRecord
end
def assets_count(except: [])
- links_count = links.count
- sources_count = except.include?(:sources) ? 0 : sources.count
+ links_count = links.size
+ sources_count = except.include?(:sources) ? 0 : sources.size
links_count + sources_count
end
@@ -123,4 +128,4 @@ class Release < ApplicationRecord
end
end
-Release.prepend_if_ee('EE::Release')
+Release.prepend_mod_with('Release')
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 98d9899a349..9c30d0611e6 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -4,6 +4,10 @@ class ReleaseHighlight
CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
+ FREE_PACKAGE = 'Free'
+ PREMIUM_PACKAGE = 'Premium'
+ ULTIMATE_PACKAGE = 'Ultimate'
+
def self.paginated(page: 1)
key = self.cache_key("items:page-#{page}")
@@ -25,14 +29,12 @@ class ReleaseHighlight
file = File.read(file_path)
items = YAML.safe_load(file, permitted_classes: [Date])
- platform = Gitlab.com? ? 'gitlab-com' : 'self-managed'
-
items&.map! do |item|
- next unless item[platform]
+ next unless include_item?(item)
begin
item.tap {|i| i['body'] = Kramdown::Document.new(i['body']).to_html }
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, file_path: file_path)
next
@@ -53,7 +55,8 @@ class ReleaseHighlight
end
def self.cache_key(key)
- ['release_highlight', key, Gitlab.revision].join(':')
+ variant = Gitlab::CurrentSettings.current_application_settings.whats_new_variant
+ ['release_highlight', variant, key, Gitlab.revision].join(':')
end
def self.next_page(current_page: 1)
@@ -88,4 +91,27 @@ class ReleaseHighlight
delegate :each, to: :items
end
+
+ def self.current_package
+ return FREE_PACKAGE unless defined?(License)
+
+ case License.current&.plan&.downcase
+ when License::PREMIUM_PLAN
+ PREMIUM_PACKAGE
+ when License::ULTIMATE_PLAN
+ ULTIMATE_PACKAGE
+ else
+ FREE_PACKAGE
+ end
+ end
+
+ def self.include_item?(item)
+ platform = Gitlab.com? ? 'gitlab-com' : 'self-managed'
+
+ return false unless item[platform]
+
+ return true unless Gitlab::CurrentSettings.current_application_settings.whats_new_variant_current_tier?
+
+ item['packages']&.include?(current_package)
+ end
end
diff --git a/app/models/releases/evidence.rb b/app/models/releases/evidence.rb
index 7c428f5ad03..5fe91b0fef5 100644
--- a/app/models/releases/evidence.rb
+++ b/app/models/releases/evidence.rb
@@ -5,7 +5,7 @@ module Releases
include ShaAttribute
include Presentable
- belongs_to :release, inverse_of: :evidences
+ belongs_to :release, inverse_of: :evidences, touch: true
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index fc2fa639f56..acc56d3980a 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -4,7 +4,7 @@ module Releases
class Link < ApplicationRecord
self.table_name = 'release_links'
- belongs_to :release
+ belongs_to :release, touch: true
# See https://gitlab.com/gitlab-org/gitlab/-/issues/218753
# Regex modified to prevent catastrophic backtracking
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c7387d2197d..c3ca90ca0ad 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -214,7 +214,7 @@ class RemoteMirror < ApplicationRecord
if super
Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
end
- rescue
+ rescue StandardError
super
end
@@ -275,7 +275,7 @@ class RemoteMirror < ApplicationRecord
return url unless ssh_key_auth? && password.present?
Gitlab::UrlSanitizer.new(read_attribute(:url), credentials: { user: user }).full_url
- rescue
+ rescue StandardError
super
end
@@ -339,4 +339,4 @@ class RemoteMirror < ApplicationRecord
end
end
-RemoteMirror.prepend_if_ee('EE::RemoteMirror')
+RemoteMirror.prepend_mod_with('RemoteMirror')
diff --git a/app/models/repository.rb b/app/models/repository.rb
index b2efc9b480b..7dca8e52403 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -995,7 +995,13 @@ class Repository
def search_files_by_wildcard_path(path, ref = 'HEAD')
# We need to use RE2 to match Gitaly's regexp engine
- regexp_string = RE2::Regexp.escape(path).gsub('\*', '.*?')
+ regexp_string = RE2::Regexp.escape(path)
+
+ anything = '.*?'
+ anything_but_not_slash = '([^\/])*?'
+ regexp_string.gsub!('\*\*', anything)
+ regexp_string.gsub!('\*', anything_but_not_slash)
+
raw_repository.search_files_by_regexp("^#{regexp_string}$", ref)
end
@@ -1165,17 +1171,13 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by do |tag|
- # Annotated tags can point to any object (e.g. a blob), but generally
- # tags point to a commit. If we don't have a commit, then just default
- # to putting the tag at the end of the list.
- target = tag.dereferenced_target
+ # Annotated tags can point to any object (e.g. a blob), but generally
+ # tags point to a commit. If we don't have a commit, then just default
+ # to putting the tag at the end of the list.
+ default = Time.current
- if target
- target.committed_date
- else
- Time.current
- end
+ tags.sort_by do |tag|
+ tag.dereferenced_target&.committed_date || default
end
end
@@ -1191,4 +1193,4 @@ class Repository
end
end
-Repository.prepend_if_ee('EE::Repository')
+Repository.prepend_mod_with('Repository')
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 57a3b568c53..68f0ab06bea 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -115,4 +115,4 @@ class ResourceLabelEvent < ResourceEvent
end
end
-ResourceLabelEvent.prepend_if_ee('EE::ResourceLabelEvent')
+ResourceLabelEvent.prepend_mod_with('ResourceLabelEvent')
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 73eb4987143..689a9d8a8ae 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -45,4 +45,4 @@ class ResourceStateEvent < ResourceEvent
end
end
-ResourceStateEvent.prepend_if_ee('EE::ResourceStateEvent')
+ResourceStateEvent.prepend_mod_with('ResourceStateEvent')
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 71077758b69..db87ff09159 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -41,4 +41,4 @@ class ResourceTimeboxEvent < ResourceEvent
end
end
-ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent')
+ResourceTimeboxEvent.prepend_mod_with('ResourceTimeboxEvent')
diff --git a/app/models/serverless/domain_cluster.rb b/app/models/serverless/domain_cluster.rb
index 9f914d5c3f8..0d54a97370e 100644
--- a/app/models/serverless/domain_cluster.rb
+++ b/app/models/serverless/domain_cluster.rb
@@ -12,7 +12,7 @@ module Serverless
attr_encrypted :key,
mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
+ key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm'
validates :pages_domain, :knative, presence: true
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index 5eca5f2bda1..8a52539d128 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -8,7 +8,7 @@ class ServiceList
end
def to_array
- [Service, columns, values]
+ [Integration, columns, values]
end
private
diff --git a/app/models/sidebars/context.rb b/app/models/sidebars/context.rb
deleted file mode 100644
index d9ac2705aaf..00000000000
--- a/app/models/sidebars/context.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# This class stores all the information needed to display and
-# render the sidebar and menus.
-# It usually stores information regarding the context and calculated
-# values where the logic is in helpers.
-module Sidebars
- class Context
- attr_reader :current_user, :container
-
- def initialize(current_user:, container:, **args)
- @current_user = current_user
- @container = container
-
- args.each do |key, value|
- singleton_class.public_send(:attr_reader, key) # rubocop:disable GitlabSecurity/PublicSend
- instance_variable_set("@#{key}", value)
- end
- end
- end
-end
diff --git a/app/models/sidebars/menu.rb b/app/models/sidebars/menu.rb
deleted file mode 100644
index a5c8be2bb31..00000000000
--- a/app/models/sidebars/menu.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class Menu
- extend ::Gitlab::Utils::Override
- include ::Gitlab::Routing
- include GitlabRoutingHelper
- include Gitlab::Allowable
- include ::Sidebars::HasPill
- include ::Sidebars::HasIcon
- include ::Sidebars::PositionableList
- include ::Sidebars::Renderable
- include ::Sidebars::ContainerWithHtmlOptions
- include ::Sidebars::HasActiveRoutes
-
- attr_reader :context
- delegate :current_user, :container, to: :@context
-
- def initialize(context)
- @context = context
- @items = []
-
- configure_menu_items
- end
-
- def configure_menu_items
- # No-op
- end
-
- override :render?
- def render?
- @items.empty? || renderable_items.any?
- end
-
- # Menus might have or not a link
- override :link
- def link
- nil
- end
-
- # This method normalizes the information retrieved from the submenus and this menu
- # Value from menus is something like: [{ path: 'foo', path: 'bar', controller: :foo }]
- # This method filters the information and returns: { path: ['foo', 'bar'], controller: :foo }
- def all_active_routes
- @all_active_routes ||= begin
- ([active_routes] + renderable_items.map(&:active_routes)).flatten.each_with_object({}) do |pairs, hash|
- pairs.each do |k, v|
- hash[k] ||= []
- hash[k] += Array(v)
- hash[k].uniq!
- end
-
- hash
- end
- end
- end
-
- def has_items?
- @items.any?
- end
-
- def add_item(item)
- add_element(@items, item)
- end
-
- def insert_item_before(before_item, new_item)
- insert_element_before(@items, before_item, new_item)
- end
-
- def insert_item_after(after_item, new_item)
- insert_element_after(@items, after_item, new_item)
- end
-
- def has_renderable_items?
- renderable_items.any?
- end
-
- def renderable_items
- @renderable_items ||= @items.select(&:render?)
- end
- end
-end
diff --git a/app/models/sidebars/menu_item.rb b/app/models/sidebars/menu_item.rb
deleted file mode 100644
index 7466b31898e..00000000000
--- a/app/models/sidebars/menu_item.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class MenuItem
- extend ::Gitlab::Utils::Override
- include ::Gitlab::Routing
- include GitlabRoutingHelper
- include Gitlab::Allowable
- include ::Sidebars::HasIcon
- include ::Sidebars::HasHint
- include ::Sidebars::Renderable
- include ::Sidebars::ContainerWithHtmlOptions
- include ::Sidebars::HasActiveRoutes
-
- attr_reader :context
-
- def initialize(context)
- @context = context
- end
- end
-end
diff --git a/app/models/sidebars/panel.rb b/app/models/sidebars/panel.rb
deleted file mode 100644
index 5c8191ebda3..00000000000
--- a/app/models/sidebars/panel.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- class Panel
- extend ::Gitlab::Utils::Override
- include ::Sidebars::PositionableList
-
- attr_reader :context, :scope_menu, :hidden_menu
-
- def initialize(context)
- @context = context
- @scope_menu = nil
- @hidden_menu = nil
- @menus = []
-
- configure_menus
- end
-
- def configure_menus
- # No-op
- end
-
- def add_menu(menu)
- add_element(@menus, menu)
- end
-
- def insert_menu_before(before_menu, new_menu)
- insert_element_before(@menus, before_menu, new_menu)
- end
-
- def insert_menu_after(after_menu, new_menu)
- insert_element_after(@menus, after_menu, new_menu)
- end
-
- def set_scope_menu(scope_menu)
- @scope_menu = scope_menu
- end
-
- def set_hidden_menu(hidden_menu)
- @hidden_menu = hidden_menu
- end
-
- def aria_label
- raise NotImplementedError
- end
-
- def has_renderable_menus?
- renderable_menus.any?
- end
-
- def renderable_menus
- @renderable_menus ||= @menus.select(&:render?)
- end
-
- def container
- context.container
- end
-
- # Auxiliar method that helps with the migration from
- # regular views to the new logic
- def render_raw_scope_menu_partial
- # No-op
- end
-
- # Auxiliar method that helps with the migration from
- # regular views to the new logic.
- #
- # Any menu inside this partial will be added after
- # all the menus added in the `configure_menus`
- # method.
- def render_raw_menus_partial
- # No-op
- end
- end
-end
diff --git a/app/models/sidebars/projects/context.rb b/app/models/sidebars/projects/context.rb
deleted file mode 100644
index 4c82309035d..00000000000
--- a/app/models/sidebars/projects/context.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- class Context < ::Sidebars::Context
- def initialize(current_user:, container:, **args)
- super(current_user: current_user, container: container, project: container, **args)
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb b/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
deleted file mode 100644
index 4b572846d1a..00000000000
--- a/app/models/sidebars/projects/menus/learn_gitlab/menu.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module LearnGitlab
- class Menu < ::Sidebars::Menu
- override :link
- def link
- project_learn_gitlab_path(context.project)
- end
-
- override :active_routes
- def active_routes
- { controller: :learn_gitlab }
- end
-
- override :title
- def title
- _('Learn GitLab')
- end
-
- override :extra_container_html_options
- def nav_link_html_options
- { class: 'home' }
- end
-
- override :sprite_icon
- def sprite_icon
- 'home'
- end
-
- override :render?
- def render?
- context.learn_gitlab_experiment_enabled
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu.rb b/app/models/sidebars/projects/menus/project_overview/menu.rb
deleted file mode 100644
index e6aa8ed159f..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- class Menu < ::Sidebars::Menu
- override :configure_menu_items
- def configure_menu_items
- add_item(MenuItems::Details.new(context))
- add_item(MenuItems::Activity.new(context))
- add_item(MenuItems::Releases.new(context))
- end
-
- override :link
- def link
- project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project rspec-project-link'
- }
- end
-
- override :extra_container_html_options
- def nav_link_html_options
- { class: 'home' }
- end
-
- override :title
- def title
- _('Project overview')
- end
-
- override :sprite_icon
- def sprite_icon
- 'home'
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
deleted file mode 100644
index 46d0f0bc43b..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/activity.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Activity < ::Sidebars::MenuItem
- override :link
- def link
- activity_project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project-activity'
- }
- end
-
- override :active_routes
- def active_routes
- { path: 'projects#activity' }
- end
-
- override :title
- def title
- _('Activity')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
deleted file mode 100644
index c40c2ed8fa2..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/details.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Details < ::Sidebars::MenuItem
- override :link
- def link
- project_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- aria: { label: _('Project details') },
- class: 'shortcuts-project'
- }
- end
-
- override :active_routes
- def active_routes
- { path: 'projects#show' }
- end
-
- override :title
- def title
- _('Details')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb b/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
deleted file mode 100644
index 5e8348f4398..00000000000
--- a/app/models/sidebars/projects/menus/project_overview/menu_items/releases.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module ProjectOverview
- module MenuItems
- class Releases < ::Sidebars::MenuItem
- override :link
- def link
- project_releases_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-project-releases'
- }
- end
-
- override :render?
- def render?
- can?(context.current_user, :read_release, context.project) && !context.project.empty_repo?
- end
-
- override :active_routes
- def active_routes
- { controller: :releases }
- end
-
- override :title
- def title
- _('Releases')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu.rb b/app/models/sidebars/projects/menus/repository/menu.rb
deleted file mode 100644
index f49a0479521..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- class Menu < ::Sidebars::Menu
- override :configure_menu_items
- def configure_menu_items
- add_item(MenuItems::Files.new(context))
- add_item(MenuItems::Commits.new(context))
- add_item(MenuItems::Branches.new(context))
- add_item(MenuItems::Tags.new(context))
- add_item(MenuItems::Contributors.new(context))
- add_item(MenuItems::Graphs.new(context))
- add_item(MenuItems::Compare.new(context))
- end
-
- override :link
- def link
- project_tree_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- class: 'shortcuts-tree'
- }
- end
-
- override :title
- def title
- _('Repository')
- end
-
- override :title_html_options
- def title_html_options
- {
- id: 'js-onboarding-repo-link'
- }
- end
-
- override :sprite_icon
- def sprite_icon
- 'doc-text'
- end
-
- override :render?
- def render?
- can?(context.current_user, :download_code, context.project) &&
- !context.project.empty_repo?
- end
- end
- end
- end
- end
-end
-
-Sidebars::Projects::Menus::Repository::Menu.prepend_if_ee('EE::Sidebars::Projects::Menus::Repository::Menu')
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/branches.rb b/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
deleted file mode 100644
index 4a62803dd2b..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/branches.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Branches < ::Sidebars::MenuItem
- override :link
- def link
- project_branches_path(context.project)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- id: 'js-onboarding-branches-link'
- }
- end
-
- override :active_routes
- def active_routes
- { controller: :branches }
- end
-
- override :title
- def title
- _('Branches')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/commits.rb b/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
deleted file mode 100644
index 647cf89133e..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/commits.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Commits < ::Sidebars::MenuItem
- override :link
- def link
- project_commits_path(context.project, context.current_ref)
- end
-
- override :extra_container_html_options
- def extra_container_html_options
- {
- id: 'js-onboarding-commits-link'
- }
- end
-
- override :active_routes
- def active_routes
- { controller: %w(commit commits) }
- end
-
- override :title
- def title
- _('Commits')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/compare.rb b/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
deleted file mode 100644
index 4812636b63f..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/compare.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Compare < ::Sidebars::MenuItem
- override :link
- def link
- project_compare_index_path(context.project, from: context.project.repository.root_ref, to: context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: :compare }
- end
-
- override :title
- def title
- _('Compare')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb b/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
deleted file mode 100644
index d60fd05bb64..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/contributors.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Contributors < ::Sidebars::MenuItem
- override :link
- def link
- project_graph_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { path: 'graphs#show' }
- end
-
- override :title
- def title
- _('Contributors')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/files.rb b/app/models/sidebars/projects/menus/repository/menu_items/files.rb
deleted file mode 100644
index 4989efe9fa5..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/files.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Files < ::Sidebars::MenuItem
- override :link
- def link
- project_tree_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: %w[tree blob blame edit_tree new_tree find_file] }
- end
-
- override :title
- def title
- _('Files')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb b/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
deleted file mode 100644
index a57021be4d0..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/graphs.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Graphs < ::Sidebars::MenuItem
- override :link
- def link
- project_network_path(context.project, context.current_ref)
- end
-
- override :active_routes
- def active_routes
- { controller: :network }
- end
-
- override :title
- def title
- _('Graph')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/repository/menu_items/tags.rb b/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
deleted file mode 100644
index d84bc89b93c..00000000000
--- a/app/models/sidebars/projects/menus/repository/menu_items/tags.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Repository
- module MenuItems
- class Tags < ::Sidebars::MenuItem
- override :link
- def link
- project_tags_path(context.project)
- end
-
- override :active_routes
- def active_routes
- { controller: :tags }
- end
-
- override :title
- def title
- _('Tags')
- end
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/menus/scope/menu.rb b/app/models/sidebars/projects/menus/scope/menu.rb
deleted file mode 100644
index 3b699083f75..00000000000
--- a/app/models/sidebars/projects/menus/scope/menu.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- module Menus
- module Scope
- class Menu < ::Sidebars::Menu
- override :link
- def link
- project_path(context.project)
- end
-
- override :title
- def title
- context.project.name
- end
- end
- end
- end
- end
-end
diff --git a/app/models/sidebars/projects/panel.rb b/app/models/sidebars/projects/panel.rb
deleted file mode 100644
index ec4fac53a40..00000000000
--- a/app/models/sidebars/projects/panel.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Sidebars
- module Projects
- class Panel < ::Sidebars::Panel
- override :configure_menus
- def configure_menus
- set_scope_menu(Sidebars::Projects::Menus::Scope::Menu.new(context))
-
- add_menu(Sidebars::Projects::Menus::ProjectOverview::Menu.new(context))
- add_menu(Sidebars::Projects::Menus::LearnGitlab::Menu.new(context))
- add_menu(Sidebars::Projects::Menus::Repository::Menu.new(context))
- end
-
- override :render_raw_menus_partial
- def render_raw_menus_partial
- 'layouts/nav/sidebar/project_menus'
- end
-
- override :aria_label
- def aria_label
- _('Project navigation')
- end
- end
- end
-end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 5fdd4551982..68957dd6b22 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -20,7 +20,6 @@ class Snippet < ApplicationRecord
extend ::Gitlab::Utils::Override
MAX_FILE_COUNT = 10
- MASTER_BRANCH = 'master'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -118,7 +117,7 @@ class Snippet < ApplicationRecord
def self.only_include_projects_visible_to(current_user = nil)
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
- joins(:project).where('projects.visibility_level IN (?)', levels)
+ joins(:project).where(projects: { visibility_level: levels })
end
def self.only_include_projects_with_snippets_enabled(include_private: false)
@@ -316,19 +315,19 @@ class Snippet < ApplicationRecord
override :default_branch
def default_branch
- super || MASTER_BRANCH
+ super || Gitlab::DefaultBranch.value(object: project)
end
def repository_storage
snippet_repository&.shard_name || Repository.pick_storage_shard
end
- # Repositories are created by default with the `master` branch.
+ # Repositories are created with a default branch. This branch
+ # can be different from the default branch set in the platform.
# This method changes the `HEAD` file to point to the existing
- # default branch in case it's not master.
+ # default branch in case it's different.
def change_head_to_default_branch
return unless repository.exists?
- return if default_branch == MASTER_BRANCH
# All snippets must have at least 1 file. Therefore, if
# `HEAD` is empty is because it's pointing to the wrong
# default branch
@@ -391,4 +390,4 @@ class Snippet < ApplicationRecord
end
end
-Snippet.prepend_if_ee('EE::Snippet')
+Snippet.prepend_mod_with('Snippet')
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index 54dbc579d54..92405a0d943 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -133,4 +133,4 @@ class SnippetRepository < ApplicationRecord
end
end
-SnippetRepository.prepend_if_ee('EE::SnippetRepository')
+SnippetRepository.prepend_mod_with('SnippetRepository')
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index 7e34988c7a0..bb928118edf 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -128,10 +128,10 @@ class SshHostKey
def normalize_url(url)
full_url = ::Addressable::URI.parse(url)
- raise ArgumentError.new("Invalid URL") unless full_url&.scheme == 'ssh'
+ raise ArgumentError, "Invalid URL" unless full_url&.scheme == 'ssh'
Addressable::URI.parse("ssh://#{full_url.host}:#{full_url.inferred_port}")
rescue Addressable::URI::InvalidURIError
- raise ArgumentError.new("Invalid URL")
+ raise ArgumentError, "Invalid URL"
end
end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index f643d52587e..092e5249a3e 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -34,7 +34,7 @@ module Storage
begin
gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
return true
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error("Exception renaming #{old_full_path} -> #{new_full_path}: #{e}")
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 20107147b4f..749b9dce97c 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -41,4 +41,4 @@ class SystemNoteMetadata < ApplicationRecord
end
end
-SystemNoteMetadata.prepend_if_ee('EE::SystemNoteMetadata')
+SystemNoteMetadata.prepend_mod_with('SystemNoteMetadata')
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index eb7d465d585..8aeeae1330c 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -104,3 +104,5 @@ module Terraform
end
end
end
+
+Terraform::State.prepend_mod
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index 432ac5b6422..31ff7e4c27d 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -20,4 +20,4 @@ module Terraform
end
end
-Terraform::StateVersion.prepend_if_ee('EE::Terraform::StateVersion')
+Terraform::StateVersion.prepend_mod_with('Terraform::StateVersion')
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index c1aa84cbbcd..bd543526685 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -3,20 +3,19 @@
class Timelog < ApplicationRecord
include Importable
+ before_save :set_project
+
validates :time_spent, :user, presence: true
validate :issuable_id_is_present, unless: :importing?
belongs_to :issue, touch: true
belongs_to :merge_request, touch: true
+ belongs_to :project
belongs_to :user
belongs_to :note
- scope :for_issues_in_group, -> (group) do
- joins(:issue).where(
- 'EXISTS (?)',
- Project.select(1).where(namespace: group.self_and_descendants)
- .where('issues.project_id = projects.id')
- )
+ scope :in_group, -> (group) do
+ joins(:project).where(projects: { namespace: group.self_and_descendants })
end
scope :between_times, -> (start_time, end_time) do
@@ -37,6 +36,10 @@ class Timelog < ApplicationRecord
end
end
+ def set_project
+ self.project_id = issuable.project_id
+ end
+
# Rails5 defaults to :touch_later, overwrite for normal touch
def belongs_to_touch_method
:touch
diff --git a/app/models/todo.rb b/app/models/todo.rb
index c8138587d83..23685fb68e0 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -149,8 +149,8 @@ class Todo < ApplicationRecord
.order('todos.created_at')
end
- def pluck_user_id
- pluck(:user_id)
+ def distinct_user_ids
+ distinct.pluck(:user_id)
end
# Count todos grouped by user_id and state, using an UNION query
@@ -252,4 +252,4 @@ class Todo < ApplicationRecord
end
end
-Todo.prepend_if_ee('EE::Todo')
+Todo.prepend_mod_with('Todo')
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 46ae924bf8c..0a4acdfc7e3 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -163,4 +163,4 @@ class Upload < ApplicationRecord
end
end
-Upload.prepend_if_ee('EE::Upload')
+Upload.prepend_mod_with('Upload')
diff --git a/app/models/user.rb b/app/models/user.rb
index 507e8cc2cf5..0eb58baae11 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -33,6 +33,8 @@ class User < ApplicationRecord
BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'
+ COUNT_CACHE_VALIDITY_PERIOD = 24.hours
+
add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) }
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token
@@ -94,6 +96,12 @@ class User < ApplicationRecord
# Virtual attribute for impersonator
attr_accessor :impersonator
+ attr_writer :max_access_for_group
+
+ def max_access_for_group
+ @max_access_for_group ||= {}
+ end
+
#
# Relations
#
@@ -197,6 +205,7 @@ class User < ApplicationRecord
has_one :user_detail
has_one :user_highest_role
has_one :user_canonical_email
+ has_one :credit_card_validation, class_name: '::Users::CreditCardValidation'
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
@@ -309,6 +318,7 @@ class User < ApplicationRecord
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
+ accepts_nested_attributes_for :credit_card_validation, update_only: true
state_machine :state, initial: :active do
event :block do
@@ -316,6 +326,7 @@ class User < ApplicationRecord
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
+ transition banned: :blocked
end
event :ldap_block do
@@ -328,17 +339,24 @@ class User < ApplicationRecord
transition blocked: :active
transition ldap_blocked: :active
transition blocked_pending_approval: :active
+ transition banned: :active
end
event :block_pending_approval do
transition active: :blocked_pending_approval
end
+ event :ban do
+ transition active: :banned
+ end
+
event :deactivate do
+ # Any additional changes to this event should be also
+ # reflected in app/workers/users/deactivate_dormant_users_worker.rb
transition active: :deactivated
end
- state :blocked, :ldap_blocked, :blocked_pending_approval do
+ state :blocked, :ldap_blocked, :blocked_pending_approval, :banned do
def blocked?
true
end
@@ -365,6 +383,7 @@ class User < ApplicationRecord
scope :instance_access_request_approvers_to_be_notified, -> { admins.active.order_recent_sign_in.limit(INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) }
+ scope :banned, -> { with_states(:banned) }
scope :external, -> { where(external: true) }
scope :non_external, -> { where(external: false) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
@@ -376,7 +395,7 @@ class User < ApplicationRecord
scope :by_name, -> (names) { iwhere(name: Array(names)) }
scope :by_user_email, -> (emails) { iwhere(email: Array(emails)) }
scope :by_emails, -> (emails) { joins(:emails).where(emails: { email: Array(emails).map(&:downcase) }) }
- scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
+ scope :for_todos, -> (todos) { where(id: todos.select(:user_id).distinct) }
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
@@ -416,10 +435,12 @@ class User < ApplicationRecord
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) }
+ scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
+ scope :with_no_activity, -> { active.where(last_activity_on: nil) }
def preferred_language
read_attribute('preferred_language') ||
- I18n.default_locale.to_s.presence_in(Gitlab::I18n::AVAILABLE_LANGUAGES.keys) ||
+ I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) ||
'en'
end
@@ -584,6 +605,8 @@ class User < ApplicationRecord
blocked
when 'blocked_pending_approval'
blocked_pending_approval
+ when 'banned'
+ banned
when 'two_factor_disabled'
without_two_factor
when 'two_factor_enabled'
@@ -1098,6 +1121,11 @@ class User < ApplicationRecord
Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !password_based_omniauth_user?
end
+ # method overriden in EE
+ def password_based_login_forbidden?
+ false
+ end
+
def can_change_username?
gitlab_config.username_changing_enabled
end
@@ -1211,6 +1239,10 @@ class User < ApplicationRecord
user_highest_role&.highest_access_level || Gitlab::Access::NO_ACCESS
end
+ def credit_card_validated_at
+ credit_card_validation&.credit_card_validated_at
+ end
+
def accessible_deploy_keys
DeployKey.from_union([
DeployKey.where(id: project_deploy_keys.select(:deploy_key_id)),
@@ -1414,7 +1446,9 @@ class User < ApplicationRecord
if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
self.errors.add(:base, :username_exists_as_a_different_namespace)
else
- self.errors[:username].concat(namespace_path_errors)
+ namespace_path_errors.each do |msg|
+ self.errors.add(:username, msg)
+ end
end
end
@@ -1619,40 +1653,32 @@ class User < ApplicationRecord
@global_notification_setting
end
- def count_cache_validity_period
- if Feature.enabled?(:longer_count_cache_validity, self, default_enabled: :yaml)
- 24.hours
- else
- 20.minutes
- end
- end
-
def assigned_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def review_requested_open_merge_requests_count(force: false)
- Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count
end
end
def assigned_open_issues_count(force: false)
- Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count
end
end
def todos_done_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
- Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: count_cache_validity_period) do
+ Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
TodosFinder.new(self, state: :pending).execute.count
end
end
@@ -1677,6 +1703,12 @@ class User < ApplicationRecord
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
+
+ if Feature.enabled?(:assigned_open_issues_cache, default_enabled: :yaml)
+ run_after_commit do
+ Users::UpdateOpenIssueCountWorker.perform_async(self.id)
+ end
+ end
end
def invalidate_merge_request_cache_counts
@@ -2061,4 +2093,4 @@ class User < ApplicationRecord
end
end
-User.prepend_if_ee('EE::User')
+User.prepend_mod_with('User')
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 0a4db707be6..8fc9efddac9 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -30,7 +30,9 @@ class UserCallout < ApplicationRecord
new_user_signups_cap_reached: 26, # EE-only
unfinished_tag_cleanup_callout: 27,
eoa_bronze_plan_banner: 28, # EE-only
- pipeline_needs_banner: 29
+ pipeline_needs_banner: 29,
+ pipeline_needs_hover_tip: 30,
+ web_ide_ci_environments_guidance: 31
}
validates :user, presence: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 6b64f583927..458764632ed 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -32,4 +32,4 @@ class UserDetail < ApplicationRecord
end
end
-UserDetail.prepend_if_ee('EE::UserDetail')
+UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 0bf8c8f901d..2735e169b5f 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -71,4 +71,4 @@ class UserPreference < ApplicationRecord
end
end
-UserPreference.prepend_if_ee('EE::UserPreference')
+UserPreference.prepend_mod_with('UserPreference')
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
new file mode 100644
index 00000000000..5e255acd882
--- /dev/null
+++ b/app/models/users/credit_card_validation.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class CreditCardValidation < ApplicationRecord
+ RELEASE_DAY = Date.new(2021, 5, 17)
+
+ self.table_name = 'user_credit_card_validations'
+
+ belongs_to :user
+ end
+end
diff --git a/app/models/users/merge_request_interaction.rb b/app/models/users/merge_request_interaction.rb
index 35d1d3206b5..4af9361fbf6 100644
--- a/app/models/users/merge_request_interaction.rb
+++ b/app/models/users/merge_request_interaction.rb
@@ -41,4 +41,4 @@ module Users
end
end
-::Users::MergeRequestInteraction.prepend_if_ee('EE::Users::MergeRequestInteraction')
+::Users::MergeRequestInteraction.prepend_mod_with('Users::MergeRequestInteraction')
diff --git a/app/models/users_statistics.rb b/app/models/users_statistics.rb
index d724b06a996..a903541f69a 100644
--- a/app/models/users_statistics.rb
+++ b/app/models/users_statistics.rb
@@ -71,4 +71,4 @@ class UsersStatistics < ApplicationRecord
end
end
-UsersStatistics.prepend_if_ee('EE::UsersStatistics')
+UsersStatistics.prepend_mod_with('UsersStatistics')
diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb
index 7728c9c174e..4e1f48227d9 100644
--- a/app/models/vulnerability.rb
+++ b/app/models/vulnerability.rb
@@ -17,4 +17,4 @@ class Vulnerability < ApplicationRecord
end
end
-Vulnerability.prepend_ee_mod
+Vulnerability.prepend_mod
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 47fe40b0e57..7fc01f373c8 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -88,7 +88,7 @@ class Wiki
repository.create_if_not_exists
raise CouldNotCreateWikiError unless repository_exists?
- rescue => err
+ rescue StandardError => err
Gitlab::ErrorTracking.track_exception(err, wiki: {
container_type: container.class.name,
container_id: container.id,
@@ -192,16 +192,9 @@ class Wiki
def delete_page(page, message = nil)
return unless page
- if Feature.enabled?(:gitaly_replace_wiki_delete_page, user, default_enabled: :yaml)
- capture_git_error(:deleted) do
- repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
+ capture_git_error(:deleted) do
+ repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
- after_wiki_activity
-
- true
- end
- else
- wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
after_wiki_activity
true
@@ -327,4 +320,4 @@ class Wiki
end
end
-Wiki.prepend_if_ee('EE::Wiki')
+Wiki.prepend_mod_with('Wiki')
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 3b9a7ded83e..9ae5a870323 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -127,10 +127,21 @@ class WikiPage
@path ||= @page.path
end
+ # Returns a CommitCollection
+ #
+ # Queries the commits for current page's path, equivalent to
+ # `git log path/to/page`. Filters and options supported:
+ # https://gitlab.com/gitlab-org/gitaly/-/blob/master/proto/commit.proto#L322-344
def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(page.path, options)
+ default_per_page = Kaminari.config.default_per_page
+ offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page)
+
+ wiki.repository.commits('HEAD',
+ path: page.path,
+ limit: options.fetch(:limit, default_per_page),
+ offset: offset)
end
def count_versions
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 1c19751cf0d..0f7a6b852ab 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_dependency 'declarative_policy'
-
class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin"
with_options scope: :user, score: 0
@@ -68,4 +66,4 @@ class BasePolicy < DeclarativePolicy::Base
condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? }
end
-BasePolicy.prepend_if_ee('EE::BasePolicy')
+BasePolicy.prepend_mod_with('BasePolicy')
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 65f2a70672b..6162a31c118 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -21,7 +21,7 @@ module Ci
end
# overridden in EE
- condition(:protected_environment_access) do
+ condition(:protected_environment) do
false
end
@@ -68,7 +68,10 @@ module Ci
rule { project_read_build }.enable :read_build_trace
rule { debug_mode & ~project_update_build }.prevent :read_build_trace
- rule { ~protected_environment_access & (protected_ref | archived) }.policy do
+ # Authorizing the user to access to protected entities.
+ # There is a "jailbreak" mode to exceptionally bypass the authorization,
+ # however, you should NEVER allow it, rather suspect it's a wrong feature/product design.
+ rule { ~can?(:jailbreak) & (archived | protected_ref | protected_environment) }.policy do
prevent :update_build
prevent :update_commit_status
prevent :erase_build
@@ -108,4 +111,4 @@ module Ci
end
end
-Ci::BuildPolicy.prepend_if_ee('EE::Ci::BuildPolicy')
+Ci::BuildPolicy.prepend_mod_with('Ci::BuildPolicy')
diff --git a/app/policies/ci/stage_policy.rb b/app/policies/ci/stage_policy.rb
new file mode 100644
index 00000000000..1e774df9f58
--- /dev/null
+++ b/app/policies/ci/stage_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class StagePolicy < BasePolicy
+ delegate :pipeline
+ end
+end
diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb
index d8e8f9ff2c1..3c5ca4bf4e1 100644
--- a/app/policies/clusters/instance_policy.rb
+++ b/app/policies/clusters/instance_policy.rb
@@ -13,4 +13,4 @@ module Clusters
end
end
-Clusters::InstancePolicy.prepend_if_ee('EE::Clusters::InstancePolicy')
+Clusters::InstancePolicy.prepend_mod_with('Clusters::InstancePolicy')
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index 75849fb10c8..cd19b46ad6c 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -82,4 +82,4 @@ module PolicyActor
end
end
-PolicyActor.prepend_if_ee('EE::PolicyActor')
+PolicyActor.prepend_mod_with('PolicyActor')
diff --git a/app/policies/concerns/readonly_abilities.rb b/app/policies/concerns/readonly_abilities.rb
index 0303d4cff14..300f17088b7 100644
--- a/app/policies/concerns/readonly_abilities.rb
+++ b/app/policies/concerns/readonly_abilities.rb
@@ -13,6 +13,7 @@ module ReadonlyAbilities
create_merge_request_from
create_merge_request_in
award_emoji
+ create_incident
].freeze
READONLY_FEATURES = %i[
@@ -49,4 +50,4 @@ module ReadonlyAbilities
end
end
-ReadonlyAbilities::ClassMethods.prepend_if_ee('EE::ReadonlyAbilities::ClassMethods')
+ReadonlyAbilities::ClassMethods.prepend_mod_with('ReadonlyAbilities::ClassMethods')
diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb
index f0187a39687..e9e3517b3da 100644
--- a/app/policies/environment_policy.rb
+++ b/app/policies/environment_policy.rb
@@ -21,4 +21,4 @@ class EnvironmentPolicy < BasePolicy
rule { ~stopped }.prevent(:destroy_environment)
end
-EnvironmentPolicy.prepend_if_ee('EE::EnvironmentPolicy')
+EnvironmentPolicy.prepend_mod_with('EnvironmentPolicy')
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index d16c4734b2c..85263ec7c87 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -113,4 +113,4 @@ class GlobalPolicy < BasePolicy
rule { external_user }.prevent :create_snippet
end
-GlobalPolicy.prepend_if_ee('EE::GlobalPolicy')
+GlobalPolicy.prepend_mod_with('GlobalPolicy')
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index 8a4cae232a0..f7a7286aba7 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -30,4 +30,4 @@ class GroupMemberPolicy < BasePolicy
end
end
-GroupMemberPolicy.prepend_if_ee('EE::GroupMemberPolicy')
+GroupMemberPolicy.prepend_mod_with('GroupMemberPolicy')
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index fc24525ade7..821fabec266 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -258,4 +258,4 @@ class GroupPolicy < BasePolicy
end
end
-GroupPolicy.prepend_if_ee('EE::GroupPolicy')
+GroupPolicy.prepend_mod_with('GroupPolicy')
diff --git a/app/policies/identity_provider_policy.rb b/app/policies/identity_provider_policy.rb
index 6d6dcaebff8..c539fc64d3f 100644
--- a/app/policies/identity_provider_policy.rb
+++ b/app/policies/identity_provider_policy.rb
@@ -14,4 +14,4 @@ class IdentityProviderPolicy < BasePolicy
rule { protected_provider }.prevent(:unlink)
end
-IdentityProviderPolicy.prepend_if_ee('EE::IdentityProviderPolicy')
+IdentityProviderPolicy.prepend_mod_with('IdentityProviderPolicy')
diff --git a/app/policies/service_policy.rb b/app/policies/integration_policy.rb
index 61aff444620..c1199d915ea 100644
--- a/app/policies/service_policy.rb
+++ b/app/policies/integration_policy.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-class ServicePolicy < BasePolicy
+class IntegrationPolicy < BasePolicy
delegate(:project)
end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index f49a6ee8498..61263e47d7c 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -28,4 +28,4 @@ class IssuablePolicy < BasePolicy
end
end
-IssuablePolicy.prepend_if_ee('EE::IssuablePolicy')
+IssuablePolicy.prepend_mod_with('IssuablePolicy')
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 183f4d8f919..6eec03d6d75 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -41,4 +41,4 @@ class IssuePolicy < IssuablePolicy
end
end
-IssuePolicy.prepend_if_ee('EE::IssuePolicy')
+IssuePolicy.prepend_mod_with('IssuePolicy')
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index e3fb54172f8..e53a916f3ca 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -29,4 +29,4 @@ class MergeRequestPolicy < IssuablePolicy
end
end
-MergeRequestPolicy.prepend_if_ee('EE::MergeRequestPolicy')
+MergeRequestPolicy.prepend_mod_with('MergeRequestPolicy')
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index 13eb4a13cac..dcbeda9f5d3 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -23,4 +23,4 @@ class NamespacePolicy < BasePolicy
rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
end
-NamespacePolicy.prepend_if_ee('EE::NamespacePolicy')
+NamespacePolicy.prepend_mod_with('NamespacePolicy')
diff --git a/app/policies/nil_policy.rb b/app/policies/nil_policy.rb
deleted file mode 100644
index fc969f8cd05..00000000000
--- a/app/policies/nil_policy.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class NilPolicy < BasePolicy
- rule { default }.prevent_all
-end
diff --git a/app/policies/packages/maven/metadatum_policy.rb b/app/policies/packages/maven/metadatum_policy.rb
new file mode 100644
index 00000000000..5dc90209321
--- /dev/null
+++ b/app/policies/packages/maven/metadatum_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Maven
+ class MetadatumPolicy < BasePolicy
+ delegate { @subject.package }
+ end
+ end
+end
diff --git a/app/policies/packages/nuget/metadatum_policy.rb b/app/policies/packages/nuget/metadatum_policy.rb
new file mode 100644
index 00000000000..cdf1283c11a
--- /dev/null
+++ b/app/policies/packages/nuget/metadatum_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Nuget
+ class MetadatumPolicy < BasePolicy
+ delegate { @subject.package }
+ end
+ end
+end
diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb
index ca33b95e523..91f1eb35506 100644
--- a/app/policies/project_member_policy.rb
+++ b/app/policies/project_member_policy.rb
@@ -8,7 +8,11 @@ class ProjectMemberPolicy < BasePolicy
condition(:project_bot) { @subject.user&.project_bot? }
rule { anonymous }.prevent_all
- rule { target_is_owner }.prevent_all
+
+ rule { target_is_owner }.policy do
+ prevent :update_project_member
+ prevent :destroy_project_member
+ end
rule { ~project_bot & can?(:admin_project_member) }.policy do
enable :update_project_member
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index c577c8c8471..1ce19511bef 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -171,6 +171,7 @@ class ProjectPolicy < BasePolicy
rule { guest | admin }.enable :read_project_for_iids
rule { admin }.enable :update_max_artifacts_size
+ rule { admin }.enable :read_storage_disk_path
rule { can?(:read_all_resources) }.enable :read_confidential_issues
rule { guest }.enable :guest_access
@@ -226,6 +227,8 @@ class ProjectPolicy < BasePolicy
enable :read_insights
end
+ rule { can?(:guest_access) & can?(:create_issue) }.enable :create_incident
+
# These abilities are not allowed to admins that are not members of the project,
# that's why they are defined separately.
rule { guest & can?(:download_code) }.enable :build_download_code
@@ -745,4 +748,4 @@ class ProjectPolicy < BasePolicy
end
end
-ProjectPolicy.prepend_if_ee('EE::ProjectPolicy')
+ProjectPolicy.prepend_mod_with('ProjectPolicy')
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 869f4716298..b8f0be9b4c5 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -51,4 +51,4 @@ class ProjectSnippetPolicy < BasePolicy
rule { ~can?(:read_snippet) }.prevent :create_note
end
-ProjectSnippetPolicy.prepend_if_ee('EE::ProjectSnippetPolicy')
+ProjectSnippetPolicy.prepend_mod_with('ProjectSnippetPolicy')
diff --git a/app/policies/protected_branch_policy.rb b/app/policies/protected_branch_policy.rb
index 1a5c6528b82..8ad06653e5c 100644
--- a/app/policies/protected_branch_policy.rb
+++ b/app/policies/protected_branch_policy.rb
@@ -10,4 +10,4 @@ class ProtectedBranchPolicy < BasePolicy
end
end
-ProtectedBranchPolicy.prepend_if_ee('EE::ProtectedBranchPolicy')
+ProtectedBranchPolicy.prepend_mod_with('ProtectedBranchPolicy')
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 48c2bd3f0bd..067f0f6a9d2 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -33,4 +33,4 @@ class UserPolicy < BasePolicy
rule { (user_is_self | admin) & ~blocked }.enable :create_user_personal_access_token
end
-UserPolicy.prepend_if_ee('EE::UserPolicy')
+UserPolicy.prepend_mod_with('UserPolicy')
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index 1cebf5c561a..c6c6fe837a0 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -112,3 +112,5 @@ module AlertManagement
end
end
end
+
+AlertManagement::AlertPresenter.prepend_mod_with('AlertManagement::AlertPresenter')
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index cff935d51b5..56dd056b9bc 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
class BlobPresenter < Gitlab::View::Presenter::Delegated
+ include ApplicationHelper
+ include BlobHelper
+ include DiffHelper
+ include TreeHelper
+ include ChecksCollaboration
+
presents :blob
def highlight(to: nil, plain: nil)
@@ -14,16 +20,68 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
)
end
+ def plain_data
+ return if blob.binary?
+
+ highlight(plain: false)
+ end
+
def web_url
- Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
+ url_helpers.project_blob_url(project, ref_qualified_path)
end
def web_path
- Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path))
+ url_helpers.project_blob_path(project, ref_qualified_path)
+ end
+
+ def edit_blob_path
+ url_helpers.project_edit_blob_path(project, ref_qualified_path)
+ end
+
+ def raw_path
+ url_helpers.project_raw_path(project, ref_qualified_path)
+ end
+
+ def replace_path
+ url_helpers.project_create_blob_path(project, ref_qualified_path)
+ end
+
+ def fork_and_edit_path
+ fork_path_for_current_user(project, edit_blob_path)
+ end
+
+ def ide_fork_and_edit_path
+ fork_path_for_current_user(project, ide_edit_path)
+ end
+
+ def can_modify_blob?
+ super(blob, project, blob.commit_id)
+ end
+
+ def ide_edit_path
+ super(project, blob.commit_id, blob.path)
+ end
+
+ def external_storage_url
+ return unless static_objects_external_storage_enabled?
+
+ external_storage_url_or_path(url_helpers.project_raw_url(project, ref_qualified_path))
end
private
+ def url_helpers
+ Gitlab::Routing.url_helpers
+ end
+
+ def project
+ blob.repository.project
+ end
+
+ def ref_qualified_path
+ File.join(blob.commit_id, blob.path)
+ end
+
def load_all_blob_data
blob.load_all_data! if blob.respond_to?(:load_all_data!)
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 51a81158f78..384cb3285fc 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -51,4 +51,4 @@ module Ci
end
end
-Ci::BuildPresenter.prepend_if_ee('EE::Ci::BuildPresenter')
+Ci::BuildPresenter.prepend_mod_with('Ci::BuildPresenter')
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 6978bc46475..5b233ad89ec 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -138,4 +138,4 @@ module Ci
end
end
-Ci::BuildRunnerPresenter.prepend_if_ee('EE::Ci::BuildRunnerPresenter')
+Ci::BuildRunnerPresenter.prepend_mod_with('Ci::BuildRunnerPresenter')
diff --git a/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb b/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
index 2fe3104fe69..d28b4523fd5 100644
--- a/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
+++ b/app/presenters/ci/pipeline_artifacts/code_quality_mr_diff_presenter.rb
@@ -5,18 +5,20 @@ module Ci
class CodeQualityMrDiffPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
- def for_files(filenames)
- quality_files = raw_report["files"].select { |key| filenames.include?(key) }
+ def for_files(merge_request)
+ filenames = merge_request.new_paths
+ mr_diff_report = raw_report(merge_request.id)
+ quality_files = mr_diff_report["files"]&.select { |key| filenames.include?(key) }
{ files: quality_files }
end
private
- def raw_report
+ def raw_report(merge_request_id)
strong_memoize(:raw_report) do
self.each_blob do |blob|
- Gitlab::Json.parse(blob).with_indifferent_access
+ Gitlab::Json.parse(blob).with_indifferent_access.fetch("merge_request_#{merge_request_id}", {})
end
end
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index a2cdabb912f..82f00f74692 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -8,15 +8,16 @@ module Ci
# We use a class method here instead of a constant, allowing EE to redefine
# the returned `Hash` more easily.
def self.failure_reasons
- { unknown_failure: 'Unknown pipeline failure!',
- config_error: 'CI/CD YAML configuration error!',
- external_validation_failure: 'External pipeline validation failed!',
- activity_limit_exceeded: 'Pipeline activity limit exceeded!',
- size_limit_exceeded: 'Pipeline size limit exceeded!',
- job_activity_limit_exceeded: 'Pipeline job activity limit exceeded!',
- deployments_limit_exceeded: 'Pipeline deployments limit exceeded!',
- project_deleted: 'The associated project was deleted',
- user_blocked: 'The user who created this pipeline is blocked' }
+ { unknown_failure: 'The reason for the pipeline failure is unknown.',
+ config_error: 'The pipeline failed due to an error on the CI/CD configuration file.',
+ external_validation_failure: 'The external pipeline validation failed.',
+ user_not_verified: 'The pipeline failed due to the user not being verified',
+ activity_limit_exceeded: 'The pipeline activity limit was exceeded.',
+ size_limit_exceeded: 'The pipeline size limit was exceeded.',
+ job_activity_limit_exceeded: 'The pipeline job activity limit was exceeded.',
+ deployments_limit_exceeded: 'The pipeline deployments limit was exceeded.',
+ project_deleted: 'The project associated with this pipeline was deleted.',
+ user_blocked: 'The user who created this pipeline is blocked.' }
end
presents :pipeline
@@ -163,4 +164,4 @@ module Ci
end
end
-Ci::PipelinePresenter.prepend_if_ee('EE::Ci::PipelinePresenter')
+Ci::PipelinePresenter.prepend_mod_with('Ci::PipelinePresenter')
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 62488465c24..a316793dae9 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -86,4 +86,4 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
end
end
-ClusterablePresenter.prepend_if_ee('EE::ClusterablePresenter')
+ClusterablePresenter.prepend_mod_with('ClusterablePresenter')
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 038fc752255..eb4bd8532af 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -76,7 +76,7 @@ module Clusters
def gitlab_managed_apps_logs_path
return unless logs_project && can_read_cluster?
- if cluster.application_elastic_stack&.available?
+ if cluster.elastic_stack_adapter&.available?
elasticsearch_project_logs_path(logs_project, cluster_id: cluster.id, format: :json)
else
k8s_project_logs_path(logs_project, cluster_id: cluster.id, format: :json)
@@ -144,4 +144,4 @@ module Clusters
end
end
-Clusters::ClusterPresenter.prepend_if_ee('EE::Clusters::ClusterPresenter')
+Clusters::ClusterPresenter.prepend_mod_with('Clusters::ClusterPresenter')
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index c8d3457b04a..8ef6e2b7962 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -15,6 +15,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator',
data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator',
forward_deployment_failure: 'The deployment job is older than the previously succeeded deployment job, and therefore cannot be run',
+ pipeline_loop_detected: 'This job could not be executed because it would create infinitely looping pipelines',
invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid',
downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found',
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
@@ -23,7 +24,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
secrets_provider_not_found: 'The secrets provider can not be found',
reached_max_descendant_pipelines_depth: 'You reached the maximum depth of child pipelines',
project_deleted: 'The job belongs to a deleted project',
- user_blocked: 'The user who created this job is blocked'
+ user_blocked: 'The user who created this job is blocked',
+ ci_quota_exceeded: 'No more CI minutes available'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@@ -39,4 +41,4 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end
end
-CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter')
+CommitStatusPresenter.prepend_mod_with('CommitStatusPresenter')
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index dfe8e315f94..adbe20517be 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -49,4 +49,4 @@ class GroupClusterablePresenter < ClusterablePresenter
end
end
-GroupClusterablePresenter.prepend_if_ee('EE::GroupClusterablePresenter')
+GroupClusterablePresenter.prepend_mod_with('GroupClusterablePresenter')
diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb
index df51f1eb075..5ab4b51f472 100644
--- a/app/presenters/group_member_presenter.rb
+++ b/app/presenters/group_member_presenter.rb
@@ -16,4 +16,4 @@ class GroupMemberPresenter < MemberPresenter
end
end
-GroupMemberPresenter.prepend_if_ee('EE::GroupMemberPresenter')
+GroupMemberPresenter.prepend_mod_with('GroupMemberPresenter')
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 94c1195ed6a..84b3328b37f 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -87,4 +87,4 @@ class InstanceClusterablePresenter < ClusterablePresenter
end
end
-InstanceClusterablePresenter.prepend_if_ee('EE::InstanceClusterablePresenter')
+InstanceClusterablePresenter.prepend_mod_with('InstanceClusterablePresenter')
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 76bf3bf4577..b7f4ac0555d 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -16,4 +16,4 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
end
end
-IssuePresenter.prepend_if_ee('EE::IssuePresenter')
+IssuePresenter.prepend_mod_with('IssuePresenter')
diff --git a/app/presenters/label_presenter.rb b/app/presenters/label_presenter.rb
index c23d6ce2218..9e51e6fa4ba 100644
--- a/app/presenters/label_presenter.rb
+++ b/app/presenters/label_presenter.rb
@@ -51,4 +51,4 @@ class LabelPresenter < Gitlab::View::Presenter::Delegated
end
end
-LabelPresenter.prepend_if_ee('EE::LabelPresenter')
+LabelPresenter.prepend_mod_with('LabelPresenter')
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 0c67fc98ced..b37a43bf251 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -52,4 +52,4 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
end
end
-MemberPresenter.prepend_if_ee('EE::MemberPresenter')
+MemberPresenter.prepend_mod_with('MemberPresenter')
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index a22138011ae..7d0fa9e2f8a 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -151,11 +151,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
def assign_to_closing_issues_link
# rubocop: disable CodeReuse/ServiceClass
- issues = MergeRequests::AssignIssuesService.new(project,
- current_user,
- merge_request: merge_request,
- closes_issues: closing_issues
- ).assignable_issues
+ issues = MergeRequests::AssignIssuesService.new(project: project,
+ current_user: current_user,
+ params: {
+ merge_request: merge_request,
+ closes_issues: closing_issues
+ }).assignable_issues
path = assign_related_issues_project_merge_request_path(project, merge_request)
if issues.present?
if issues.count > 1
@@ -273,4 +274,4 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
-MergeRequestPresenter.prepend_if_ee('EE::MergeRequestPresenter')
+MergeRequestPresenter.prepend_mod_with('MergeRequestPresenter')
diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb
index 6640b0c5e94..4fa207b1205 100644
--- a/app/presenters/packages/detail/package_presenter.rb
+++ b/app/presenters/packages/detail/package_presenter.rb
@@ -17,6 +17,7 @@ module Packages
name: name,
package_files: @package.package_files.map { |pf| build_package_file_view(pf) },
package_type: @package.package_type,
+ status: @package.status,
project_id: @package.project_id,
tags: @package.tags.as_json,
updated_at: @package.updated_at,
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 718f653eab1..1c5f11ffe59 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -44,4 +44,4 @@ class ProjectClusterablePresenter < ClusterablePresenter
end
end
-ProjectClusterablePresenter.prepend_if_ee('EE::ProjectClusterablePresenter')
+ProjectClusterablePresenter.prepend_mod_with('ProjectClusterablePresenter')
diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb
index ff9c3df793a..17947266ed7 100644
--- a/app/presenters/project_member_presenter.rb
+++ b/app/presenters/project_member_presenter.rb
@@ -16,4 +16,4 @@ class ProjectMemberPresenter < MemberPresenter
end
end
-ProjectMemberPresenter.prepend_if_ee('EE::ProjectMemberPresenter')
+ProjectMemberPresenter.prepend_mod_with('ProjectMemberPresenter')
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index aad1c816cf1..4f803ba34f4 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -108,7 +108,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_license_ide_path
- ide_edit_path(project, default_branch_or_master, 'LICENSE')
+ ide_edit_path(project, default_branch_or_main, 'LICENSE')
end
def add_changelog_path
@@ -116,7 +116,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_changelog_ide_path
- ide_edit_path(project, default_branch_or_master, 'CHANGELOG')
+ ide_edit_path(project, default_branch_or_main, 'CHANGELOG')
end
def add_contribution_guide_path
@@ -124,7 +124,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_contribution_guide_ide_path
- ide_edit_path(project, default_branch_or_master, 'CONTRIBUTING.md')
+ ide_edit_path(project, default_branch_or_main, 'CONTRIBUTING.md')
end
def add_readme_path
@@ -132,13 +132,24 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_readme_ide_path
- ide_edit_path(project, default_branch_or_master, 'README.md')
+ ide_edit_path(project, default_branch_or_main, 'README.md')
end
def add_ci_yml_path
add_special_file_path(file_name: ci_config_path_or_default)
end
+ def add_code_quality_ci_yml_path
+ add_special_file_path(
+ file_name: ci_config_path_or_default,
+ commit_message: s_("CommitMessage|Add %{file_name} and create a code quality job") % { file_name: ci_config_path_or_default },
+ additional_params: {
+ template: 'Code-Quality',
+ code_quality_walkthrough: true
+ }
+ )
+ end
+
def license_short_name
license = repository.license
license&.nickname || license&.name || 'LICENSE'
@@ -210,7 +221,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
- empty_repo? ? nil : project_commits_path(project, repository.root_ref))
+ empty_repo? ? nil : project_commits_path(project, default_branch_or_main))
end
def branches_anchor_data
@@ -249,10 +260,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
nil,
nil,
{
- 'target_branch' => default_branch_or_master,
- 'original_branch' => default_branch_or_master,
+ 'target_branch' => default_branch_or_main,
+ 'original_branch' => default_branch_or_main,
'can_push_code' => 'true',
- 'path' => project_create_blob_path(project, default_branch_or_master),
+ 'path' => project_create_blob_path(project, default_branch_or_main),
'project_path' => project.path
}
)
@@ -268,7 +279,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def new_file_anchor_data
if can_current_user_push_to_default_branch?
- new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master)
+ new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_main) : project_new_blob_path(project, default_branch_or_main)
AnchorData.new(false,
statistic_icon + _('New file'),
@@ -390,16 +401,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def topics_to_show
- project.tag_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord
+ project.topic_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord
end
def topics_not_shown
- project.tag_list - topics_to_show
+ project.topic_list - topics_to_show
end
def count_of_extra_topics_not_shown
- if project.tag_list.count > MAX_TOPICS_TO_SHOW
- project.tag_list.count - MAX_TOPICS_TO_SHOW
+ if project.topic_list.count > MAX_TOPICS_TO_SHOW
+ project.topic_list.count - MAX_TOPICS_TO_SHOW
else
0
end
@@ -468,16 +479,17 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
end
- def add_special_file_path(file_name:, commit_message: nil, branch_name: nil)
+ def add_special_file_path(file_name:, commit_message: nil, branch_name: nil, additional_params: {})
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
- default_branch_or_master,
+ default_branch_or_main,
file_name: file_name,
commit_message: commit_message,
- branch_name: branch_name
+ branch_name: branch_name,
+ **additional_params
)
end
end
-ProjectPresenter.prepend_if_ee('EE::ProjectPresenter')
+ProjectPresenter.prepend_mod_with('ProjectPresenter')
diff --git a/app/presenters/projects/import_export/project_export_presenter.rb b/app/presenters/projects/import_export/project_export_presenter.rb
index b52f3411c49..611294ddfd8 100644
--- a/app/presenters/projects/import_export/project_export_presenter.rb
+++ b/app/presenters/projects/import_export/project_export_presenter.rb
@@ -31,11 +31,11 @@ module Projects
def group_members
return [] unless current_user.can?(:admin_group, project.group)
- # We need `.where.not(user_id: nil)` here otherwise when a group has an
+ # We need `.connected_to_user` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
- non_null_user_ids = project.project_members.where.not(user_id: nil).select(:user_id)
+ non_null_user_ids = project.project_members.connected_to_user.select(:user_id)
GroupMembersFinder.new(project.group).execute.where.not(user_id: non_null_user_ids)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/presenters/service_hook_presenter.rb b/app/presenters/service_hook_presenter.rb
index bc20d5b1a3b..8f2ba1a905f 100644
--- a/app/presenters/service_hook_presenter.rb
+++ b/app/presenters/service_hook_presenter.rb
@@ -4,10 +4,10 @@ class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
presents :service_hook
def logs_details_path(log)
- project_service_hook_log_path(service.project, service, log)
+ project_service_hook_log_path(integration.project, integration, log)
end
def logs_retry_path(log)
- retry_project_service_hook_log_path(service.project, service, log)
+ retry_project_service_hook_log_path(integration.project, integration, log)
end
end
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index 597ef6ebc39..e9c710e4a0f 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -9,12 +9,6 @@ class SnippetBlobPresenter < BlobPresenter
render_rich_partial
end
- def plain_data
- return if blob.binary?
-
- highlight(plain: false)
- end
-
def raw_path
snippet_blob_raw_route(only_path: true)
end
diff --git a/app/presenters/terraform/modules_presenter.rb b/app/presenters/terraform/modules_presenter.rb
new file mode 100644
index 00000000000..608f69e2019
--- /dev/null
+++ b/app/presenters/terraform/modules_presenter.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Terraform
+ class ModulesPresenter < Gitlab::View::Presenter::Simple
+ attr_accessor :packages, :system
+
+ presents :modules
+
+ def initialize(packages, system)
+ @packages = packages
+ @system = system
+ end
+
+ def modules
+ project_url = @packages.first&.project&.web_url
+ versions = @packages.map do |package|
+ {
+ 'version' => package.version,
+ 'submodules' => [],
+ 'root' => {
+ 'dependencies' => [],
+ 'providers' => [
+ {
+ 'name' => @system,
+ 'version' => ''
+ }
+ ]
+ }
+ }
+ end
+
+ [
+ {
+ 'versions' => versions,
+ 'source' => project_url
+ }.compact
+ ]
+ end
+ end
+end
diff --git a/app/serializers/admin/user_entity.rb b/app/serializers/admin/user_entity.rb
index a5cf40a50b9..8fab1fa3a3e 100644
--- a/app/serializers/admin/user_entity.rb
+++ b/app/serializers/admin/user_entity.rb
@@ -31,4 +31,4 @@ module Admin
end
end
-Admin::UserEntity.prepend_if_ee('EE::Admin::UserEntity')
+Admin::UserEntity.prepend_mod_with('Admin::UserEntity')
diff --git a/app/serializers/analytics/cycle_analytics/configuration_entity.rb b/app/serializers/analytics/cycle_analytics/configuration_entity.rb
new file mode 100644
index 00000000000..45ea7c92758
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/configuration_entity.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ConfigurationEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :events, using: Analytics::CycleAnalytics::EventEntity
+ expose :stages, using: Analytics::CycleAnalytics::StageEntity
+
+ private
+
+ def events
+ (stage_events.events - stage_events.internal_events).sort_by(&:name)
+ end
+
+ def stage_events
+ Gitlab::Analytics::CycleAnalytics::StageEvents
+ end
+ end
+ end
+end
diff --git a/app/serializers/analytics/cycle_analytics/event_entity.rb b/app/serializers/analytics/cycle_analytics/event_entity.rb
new file mode 100644
index 00000000000..b9abf722c8d
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/event_entity.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class EventEntity < Grape::Entity
+ expose :name
+ expose :identifier
+ expose :type
+ expose :can_be_start_event?, as: :can_be_start_event
+ expose :allowed_end_events
+
+ private
+
+ def type
+ object.label_based? ? 'label' : 'simple'
+ end
+
+ def can_be_start_event?
+ pairing_rules.has_key?(object)
+ end
+
+ def allowed_end_events
+ pairing_rules.fetch(object, []).map do |event|
+ event.identifier unless stage_events.internal_events.include?(event)
+ end.compact
+ end
+
+ def pairing_rules
+ stage_events.pairing_rules
+ end
+
+ def stage_events
+ Gitlab::Analytics::CycleAnalytics::StageEvents
+ end
+ end
+ end
+end
diff --git a/app/serializers/analytics/cycle_analytics/stage_entity.rb b/app/serializers/analytics/cycle_analytics/stage_entity.rb
new file mode 100644
index 00000000000..b24148802d0
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/stage_entity.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageEntity < Grape::Entity
+ expose :title
+ expose :hidden
+ expose :legend
+ expose :description
+ expose :id
+ expose :custom
+ expose :start_event_identifier, if: -> (s) { s.custom? }
+ expose :end_event_identifier, if: -> (s) { s.custom? }
+ expose :start_event_label, using: LabelEntity, if: -> (s) { s.start_event_label_based? }
+ expose :end_event_label, using: LabelEntity, if: -> (s) { s.end_event_label_based? }
+ expose :start_event_html_description
+ expose :end_event_html_description
+
+ def id
+ object.id || object.name
+ end
+
+ def start_event_html_description
+ html_description(object.start_event)
+ end
+
+ def end_event_html_description
+ html_description(object.end_event)
+ end
+
+ private
+
+ def html_description(event)
+ Banzai::Renderer.render(event.markdown_description, { group: object.group, project: nil })
+ end
+ end
+ end
+end
diff --git a/app/serializers/analytics/cycle_analytics/value_stream_entity.rb b/app/serializers/analytics/cycle_analytics/value_stream_entity.rb
new file mode 100644
index 00000000000..1943efcc63d
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/value_stream_entity.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ValueStreamEntity < Grape::Entity
+ expose :name
+ expose :id
+ expose :is_custom do |object|
+ object.custom?
+ end
+ expose :stages, using: Analytics::CycleAnalytics::StageEntity
+
+ private
+
+ def id
+ object.id || object.name # use the name `default` if the record is not persisted
+ end
+
+ def stages
+ object.stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) } # rubocop: disable CodeReuse/Presenter
+ end
+ end
+ end
+end
diff --git a/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb b/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb
new file mode 100644
index 00000000000..ffd7aa882e4
--- /dev/null
+++ b/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class ValueStreamSerializer < BaseSerializer
+ entity ::Analytics::CycleAnalytics::ValueStreamEntity
+ end
+ end
+end
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
index a54af899ba2..6dde35a9415 100644
--- a/app/serializers/blob_entity.rb
+++ b/app/serializers/blob_entity.rb
@@ -16,4 +16,4 @@ class BlobEntity < Grape::Entity
end
end
-BlobEntity.prepend_if_ee('EE::BlobEntity')
+BlobEntity.prepend_mod_with('BlobEntity')
diff --git a/app/serializers/board_simple_entity.rb b/app/serializers/board_simple_entity.rb
index a3c16a0b5c7..ab625490966 100644
--- a/app/serializers/board_simple_entity.rb
+++ b/app/serializers/board_simple_entity.rb
@@ -5,4 +5,4 @@ class BoardSimpleEntity < Grape::Entity
expose :name
end
-BoardSimpleEntity.prepend_if_ee('EE::BoardSimpleEntity')
+BoardSimpleEntity.prepend_mod_with('BoardSimpleEntity')
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 0ddcad4dcb9..6fbd14f523d 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -155,4 +155,4 @@ class BuildDetailsEntity < JobEntity
end
end
-BuildDetailsEntity.prepend_if_ee('::EE::BuildDetailEntity')
+BuildDetailsEntity.prepend_mod_with('BuildDetailEntity')
diff --git a/app/serializers/ci/downloadable_artifact_entity.rb b/app/serializers/ci/downloadable_artifact_entity.rb
new file mode 100644
index 00000000000..1f3885f0715
--- /dev/null
+++ b/app/serializers/ci/downloadable_artifact_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Ci
+ class DownloadableArtifactEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :artifacts do |pipeline, options|
+ artifacts = pipeline.downloadable_artifacts
+
+ if Feature.enabled?(:non_public_artifacts)
+ artifacts = artifacts.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) }
+ end
+
+ BuildArtifactEntity.represent(artifacts, options.merge(project: pipeline.project))
+ end
+ end
+end
diff --git a/app/serializers/ci/downloadable_artifact_serializer.rb b/app/serializers/ci/downloadable_artifact_serializer.rb
new file mode 100644
index 00000000000..fc4c9fa558e
--- /dev/null
+++ b/app/serializers/ci/downloadable_artifact_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class DownloadableArtifactSerializer < BaseSerializer
+ entity DownloadableArtifactEntity
+ end
+end
diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb
index 743643a978f..fa0e904fbde 100644
--- a/app/serializers/ci/pipeline_entity.rb
+++ b/app/serializers/ci/pipeline_entity.rb
@@ -121,4 +121,4 @@ class Ci::PipelineEntity < Grape::Entity
end
end
-Ci::PipelineEntity.prepend_if_ee('EE::Ci::PipelineEntity')
+Ci::PipelineEntity.prepend_mod_with('Ci::PipelineEntity')
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index b904666971e..ba42e14be22 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -28,6 +28,6 @@ class ClusterEntity < Grape::Entity
end
expose :enable_advanced_logs_querying do |cluster|
- cluster.application_elastic_stack_available?
+ cluster.elastic_stack_available?
end
end
diff --git a/app/serializers/context_commits_diff_entity.rb b/app/serializers/context_commits_diff_entity.rb
new file mode 100644
index 00000000000..89ebf6a4815
--- /dev/null
+++ b/app/serializers/context_commits_diff_entity.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class ContextCommitsDiffEntity < Grape::Entity
+ include Gitlab::Routing
+
+ expose :commits_count
+
+ expose :showing_context_commits_diff do |_, options|
+ options[:only_context_commits]
+ end
+
+ expose :diffs_path do |diff|
+ merge_request = diff.merge_request
+ project = merge_request.target_project
+
+ next unless project
+
+ diffs_project_merge_request_path(project, merge_request, only_context_commits: true)
+ end
+end
diff --git a/app/serializers/current_board_entity.rb b/app/serializers/current_board_entity.rb
index 08f31bc698f..530f7f5dea3 100644
--- a/app/serializers/current_board_entity.rb
+++ b/app/serializers/current_board_entity.rb
@@ -7,4 +7,4 @@ class CurrentBoardEntity < Grape::Entity
expose :hide_closed_list
end
-CurrentBoardEntity.prepend_if_ee('EE::CurrentBoardEntity')
+CurrentBoardEntity.prepend_mod_with('CurrentBoardEntity')
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index a37011d0100..08a939e86c5 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -38,7 +38,7 @@ class DeploymentEntity < Grape::Entity
expose :commit, using: CommitEntity, if: -> (*) { include_details? }
expose :manual_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
expose :scheduled_actions, using: JobEntity, if: -> (*) { include_details? && can_create_deployment? }
- expose :playable_build, expose_nil: false, if: -> (*) { include_details? && can_create_deployment? } do |deployment, options|
+ expose :playable_build, if: -> (deployment) { include_details? && can_create_deployment? && deployment.playable_build } do |deployment, options|
JobEntity.represent(deployment.playable_build, options.merge(only: [:play_path, :retry_path]))
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 4bc6644a5cb..64f7f8bb5eb 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -84,6 +84,15 @@ class DiffsEntity < Grape::Entity
project_blob_path(merge_request.project, merge_request.diff_head_sha)
end
+ expose :context_commits_diff, if: -> (_) { merge_request&.project&.context_commits_enabled? } do |diffs, options|
+ next unless merge_request.context_commits_diff.commits_count > 0
+
+ ContextCommitsDiffEntity.represent(
+ merge_request.context_commits_diff,
+ options
+ )
+ end
+
def merge_request
options[:merge_request]
end
diff --git a/app/serializers/discussion_serializer.rb b/app/serializers/discussion_serializer.rb
index 7e7148b046e..8bae5687a64 100644
--- a/app/serializers/discussion_serializer.rb
+++ b/app/serializers/discussion_serializer.rb
@@ -18,4 +18,4 @@ class DiscussionSerializer < BaseSerializer
end
end
-DiscussionSerializer.prepend_if_ee('EE::DiscussionSerializer')
+DiscussionSerializer.prepend_mod_with('DiscussionSerializer')
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 66ca2382901..6105b52fbda 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -21,7 +21,7 @@ class EnvironmentEntity < Grape::Entity
expose :stop_action_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
- expose :upcoming_deployment, expose_nil: false do |environment, ops|
+ expose :upcoming_deployment, if: -> (environment) { environment.upcoming_deployment } do |environment, ops|
DeploymentEntity.represent(environment.upcoming_deployment,
ops.merge(except: UNNECESSARY_ENTRIES_FOR_UPCOMING_DEPLOYMENT))
end
@@ -122,4 +122,4 @@ class EnvironmentEntity < Grape::Entity
end
end
-EnvironmentEntity.prepend_if_ee('::EE::EnvironmentEntity')
+EnvironmentEntity.prepend_mod_with('EnvironmentEntity')
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 2bb9a7e7254..2fb1ad52135 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -23,6 +23,8 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) }
end
else
+ resource = @paginator.paginate(resource) if paginated?
+
super(batch_load(resource), opts)
end
end
@@ -52,7 +54,7 @@ class EnvironmentSerializer < BaseSerializer
def batch_load(resource)
resource = resource.preload(environment_associations)
- resource.all.tap do |environments|
+ resource.all.to_a.tap do |environments|
environments.each do |environment|
# Batch loading the commits of the deployments
environment.last_deployment&.commit&.try(:lazy_author)
@@ -96,4 +98,4 @@ class EnvironmentSerializer < BaseSerializer
# rubocop: enable CodeReuse/ActiveRecord
end
-EnvironmentSerializer.prepend_if_ee('EE::EnvironmentSerializer')
+EnvironmentSerializer.prepend_mod_with('EnvironmentSerializer')
diff --git a/app/serializers/evidences/release_entity.rb b/app/serializers/evidences/release_entity.rb
index dfc4f52de07..3b16347d7c0 100644
--- a/app/serializers/evidences/release_entity.rb
+++ b/app/serializers/evidences/release_entity.rb
@@ -12,4 +12,4 @@ module Evidences
end
end
-Evidences::ReleaseEntity.prepend_if_ee('EE::Evidences::ReleaseEntity')
+Evidences::ReleaseEntity.prepend_mod_with('Evidences::ReleaseEntity')
diff --git a/app/serializers/fork_namespace_entity.rb b/app/serializers/fork_namespace_entity.rb
index fc238fa3958..fbcdf91a1af 100644
--- a/app/serializers/fork_namespace_entity.rb
+++ b/app/serializers/fork_namespace_entity.rb
@@ -49,4 +49,4 @@ class ForkNamespaceEntity < Grape::Entity
end
end
-ForkNamespaceEntity.prepend_if_ee('EE::ForkNamespaceEntity')
+ForkNamespaceEntity.prepend_mod_with('ForkNamespaceEntity')
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index adbda790dee..619ca0b5f82 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -100,4 +100,4 @@ class GroupChildEntity < Grape::Entity
end
end
-GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
+GroupChildEntity.prepend_mod_with('GroupChildEntity')
diff --git a/app/serializers/group_issuable_autocomplete_entity.rb b/app/serializers/group_issuable_autocomplete_entity.rb
new file mode 100644
index 00000000000..f950a7db785
--- /dev/null
+++ b/app/serializers/group_issuable_autocomplete_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class GroupIssuableAutocompleteEntity < Grape::Entity
+ expose :iid
+ expose :title
+ expose :reference do |issuable, options|
+ issuable.to_reference(options[:parent_group])
+ end
+end
diff --git a/app/serializers/group_issuable_autocomplete_serializer.rb b/app/serializers/group_issuable_autocomplete_serializer.rb
new file mode 100644
index 00000000000..59e9201d405
--- /dev/null
+++ b/app/serializers/group_issuable_autocomplete_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class GroupIssuableAutocompleteSerializer < BaseSerializer
+ entity GroupIssuableAutocompleteEntity
+end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index 4b3d6f21d6d..14e416fb71a 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -118,4 +118,4 @@ class IssuableSidebarBasicEntity < Grape::Entity
end
end
-IssuableSidebarBasicEntity.prepend_if_ee('EE::IssuableSidebarBasicEntity')
+IssuableSidebarBasicEntity.prepend_mod_with('IssuableSidebarBasicEntity')
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index ea629d9d774..17a36f5fb07 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -20,7 +20,7 @@ class IssueBoardEntity < Grape::Entity
API::Entities::Project.represent issue.project, only: [:id, :path]
end
- expose :milestone, expose_nil: false do |issue|
+ expose :milestone, if: -> (issue) { issue.milestone } do |issue|
API::Entities::Milestone.represent issue.milestone, only: [:id, :title]
end
@@ -53,4 +53,4 @@ class IssueBoardEntity < Grape::Entity
end
end
-IssueBoardEntity.prepend_if_ee('EE::IssueBoardEntity')
+IssueBoardEntity.prepend_mod_with('IssueBoardEntity')
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 647a73495f8..773bbf268eb 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -77,4 +77,4 @@ class IssueEntity < IssuableEntity
end
end
-IssueEntity.prepend_if_ee('::EE::IssueEntity')
+IssueEntity.prepend_mod_with('IssueEntity')
diff --git a/app/serializers/issue_sidebar_basic_entity.rb b/app/serializers/issue_sidebar_basic_entity.rb
index e9e05718af9..f93a42e5f98 100644
--- a/app/serializers/issue_sidebar_basic_entity.rb
+++ b/app/serializers/issue_sidebar_basic_entity.rb
@@ -6,4 +6,4 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
expose :severity
end
-IssueSidebarBasicEntity.prepend_if_ee('EE::IssueSidebarBasicEntity')
+IssueSidebarBasicEntity.prepend_mod_with('IssueSidebarBasicEntity')
diff --git a/app/serializers/issue_sidebar_extras_entity.rb b/app/serializers/issue_sidebar_extras_entity.rb
index ea2a49baa41..6fd1045882b 100644
--- a/app/serializers/issue_sidebar_extras_entity.rb
+++ b/app/serializers/issue_sidebar_extras_entity.rb
@@ -3,4 +3,4 @@
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
end
-IssueSidebarExtrasEntity.prepend_if_ee('EE::IssueSidebarExtrasEntity')
+IssueSidebarExtrasEntity.prepend_mod_with('IssueSidebarExtrasEntity')
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index d05b500b140..eb8622edb38 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity
expose :name
expose :started?, as: :started
+ expose :complete?, as: :complete
expose :archived?, as: :archived
# bridge jobs don't have build detail pages
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 6cbdaeea5ea..7559a03bd3b 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -57,4 +57,4 @@ class MemberEntity < Grape::Entity
end
end
-MemberEntity.prepend_if_ee('EE::MemberEntity')
+MemberEntity.prepend_mod_with('MemberEntity')
diff --git a/app/serializers/member_user_entity.rb b/app/serializers/member_user_entity.rb
index a022966c041..01920fc95bb 100644
--- a/app/serializers/member_user_entity.rb
+++ b/app/serializers/member_user_entity.rb
@@ -25,4 +25,4 @@ class MemberUserEntity < UserEntity
end
end
-MemberUserEntity.prepend_if_ee('EE::MemberUserEntity')
+MemberUserEntity.prepend_mod_with('MemberUserEntity')
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index 52f5b975656..6ac43e02f3c 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -187,4 +187,4 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
end
end
-MergeRequestPollCachedWidgetEntity.prepend_if_ee('EE::MergeRequestPollCachedWidgetEntity')
+MergeRequestPollCachedWidgetEntity.prepend_mod_with('MergeRequestPollCachedWidgetEntity')
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 97a81d8170f..c00dceadf22 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -158,4 +158,4 @@ class MergeRequestPollWidgetEntity < Grape::Entity
end
end
-MergeRequestPollWidgetEntity.prepend_if_ee('EE::MergeRequestPollWidgetEntity')
+MergeRequestPollWidgetEntity.prepend_mod_with('MergeRequestPollWidgetEntity')
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 508a2510dbd..e8fc18e6cf3 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -28,4 +28,4 @@ class MergeRequestSerializer < BaseSerializer
end
end
-MergeRequestSerializer.prepend_if_ee('EE::MergeRequestSerializer')
+MergeRequestSerializer.prepend_mod_with('MergeRequestSerializer')
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index a36c4da3e83..66672494bd9 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -29,4 +29,4 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
end
end
-MergeRequestUserEntity.prepend_if_ee('EE::MergeRequestUserEntity')
+MergeRequestUserEntity.prepend_mod_with('MergeRequestUserEntity')
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index a168c7a8490..ac9970579ed 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -52,7 +52,7 @@ class MergeRequestWidgetEntity < Grape::Entity
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")
+ help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
end
expose :merge_request_pipelines_docs_path do |merge_request|
@@ -176,4 +176,4 @@ class MergeRequestWidgetEntity < Grape::Entity
end
end
-MergeRequestWidgetEntity.prepend_if_ee('EE::MergeRequestWidgetEntity')
+MergeRequestWidgetEntity.prepend_mod_with('MergeRequestWidgetEntity')
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index d44958bc0c4..8308e954c06 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -103,4 +103,4 @@ class NoteEntity < API::Entities::Note
end
end
-NoteEntity.prepend_if_ee('EE::NoteEntity')
+NoteEntity.prepend_mod_with('NoteEntity')
diff --git a/app/serializers/note_user_entity.rb b/app/serializers/note_user_entity.rb
index 38e71528f18..c3f14fb0f9e 100644
--- a/app/serializers/note_user_entity.rb
+++ b/app/serializers/note_user_entity.rb
@@ -4,4 +4,4 @@ class NoteUserEntity < UserEntity
unexpose :web_url
end
-NoteUserEntity.prepend_if_ee('EE::NoteUserEntity')
+NoteUserEntity.prepend_mod_with('NoteUserEntity')
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index bb6aa2f78ac..f459e700c03 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -8,17 +8,13 @@ class PipelineDetailsEntity < Ci::PipelineEntity
end
expose :details do
- expose :artifacts do |pipeline, options|
- rel = pipeline.downloadable_artifacts
-
- if Feature.enabled?(:non_public_artifacts, type: :development)
- rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) }
- end
-
- BuildArtifactEntity.represent(rel, options.merge(project: pipeline.project))
- end
expose :manual_actions, using: BuildActionEntity
expose :scheduled_actions, using: BuildActionEntity
+ expose :code_quality_build_path, if: -> (_, options) { options[:code_quality_walkthrough] } do |pipeline|
+ next unless code_quality_build = pipeline.builds.finished.find_by_name('code_quality')
+
+ project_job_path(pipeline.project, code_quality_build, code_quality_walkthrough: true)
+ end
end
expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 9a2e29a6ee3..9cfc81e8705 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -49,10 +49,6 @@ class PipelineSerializer < BaseSerializer
{
manual_actions: :metadata,
scheduled_actions: :metadata,
- downloadable_artifacts: {
- project: [:route, { namespace: :route }],
- job: []
- },
failed_builds: %i(project metadata),
merge_request: {
source_project: [:route, { namespace: :route }],
@@ -74,4 +70,4 @@ class PipelineSerializer < BaseSerializer
end
end
-PipelineSerializer.prepend_if_ee('EE::PipelineSerializer')
+PipelineSerializer.prepend_mod_with('PipelineSerializer')
diff --git a/app/serializers/project_mirror_entity.rb b/app/serializers/project_mirror_entity.rb
index daea209deb4..215f659caba 100644
--- a/app/serializers/project_mirror_entity.rb
+++ b/app/serializers/project_mirror_entity.rb
@@ -8,4 +8,4 @@ class ProjectMirrorEntity < Grape::Entity
end
end
-ProjectMirrorEntity.prepend_if_ee('::EE::ProjectMirrorEntity')
+ProjectMirrorEntity.prepend_mod_with('ProjectMirrorEntity')
diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb
index 299160cd1bf..0e64b843fd3 100644
--- a/app/serializers/test_case_entity.rb
+++ b/app/serializers/test_case_entity.rb
@@ -20,6 +20,6 @@ class TestCaseEntity < Grape::Entity
alias_method :test_case, :object
def can_read_screenshots?
- Feature.enabled?(:junit_pipeline_screenshots_view, options[:project]) && test_case.has_attachment?
+ test_case.has_attachment?
end
end
diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb
index 9386c06b87a..1a2778cbf30 100644
--- a/app/serializers/user_entity.rb
+++ b/app/serializers/user_entity.rb
@@ -3,4 +3,4 @@
class UserEntity < API::Entities::UserPath
end
-UserEntity.prepend_if_ee('EE::UserEntity')
+UserEntity.prepend_mod_with('UserEntity')
diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb
index 4a5dda1e364..24c5bccdeb9 100644
--- a/app/serializers/user_preference_entity.rb
+++ b/app/serializers/user_preference_entity.rb
@@ -13,4 +13,4 @@ class UserPreferenceEntity < Grape::Entity
end
end
-UserPreferenceEntity.prepend_if_ee('EE::UserPreferenceEntity')
+UserPreferenceEntity.prepend_mod_with('UserPreferenceEntity')
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index dfbd787298d..5e7dab31e8a 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -21,4 +21,4 @@ class UserSerializer < BaseSerializer
end
end
-UserSerializer.prepend_if_ee('EE::UserSerializer')
+UserSerializer.prepend_mod_with('UserSerializer')
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb
index 253c3a84fef..f7a4bf1a9f9 100644
--- a/app/services/admin/propagate_integration_service.rb
+++ b/app/services/admin/propagate_integration_service.rb
@@ -5,7 +5,7 @@ module Admin
include PropagateService
def propagate
- if integration.instance?
+ if integration.instance_level?
update_inherited_integrations
create_integration_for_groups_without_integration
create_integration_for_projects_without_integration
@@ -20,14 +20,14 @@ module Admin
def update_inherited_integrations
propagate_integrations(
- Service.by_type(integration.type).inherit_from_id(integration.id),
+ Integration.by_type(integration.type).inherit_from_id(integration.id),
PropagateIntegrationInheritWorker
)
end
def update_inherited_descendant_integrations
propagate_integrations(
- Service.inherited_descendants_from_self_or_ancestors_from(integration),
+ Integration.inherited_descendants_from_self_or_ancestors_from(integration),
PropagateIntegrationInheritDescendantWorker
)
end
diff --git a/app/services/alert_management/http_integrations/create_service.rb b/app/services/alert_management/http_integrations/create_service.rb
index e7f1084ce5c..1abe0548c45 100644
--- a/app/services/alert_management/http_integrations/create_service.rb
+++ b/app/services/alert_management/http_integrations/create_service.rb
@@ -66,4 +66,4 @@ module AlertManagement
end
end
-::AlertManagement::HttpIntegrations::CreateService.prepend_if_ee('::EE::AlertManagement::HttpIntegrations::CreateService')
+::AlertManagement::HttpIntegrations::CreateService.prepend_mod_with('AlertManagement::HttpIntegrations::CreateService')
diff --git a/app/services/alert_management/http_integrations/update_service.rb b/app/services/alert_management/http_integrations/update_service.rb
index af079f670b8..8662f966a2e 100644
--- a/app/services/alert_management/http_integrations/update_service.rb
+++ b/app/services/alert_management/http_integrations/update_service.rb
@@ -56,4 +56,4 @@ module AlertManagement
end
end
-::AlertManagement::HttpIntegrations::UpdateService.prepend_if_ee('::EE::AlertManagement::HttpIntegrations::UpdateService')
+::AlertManagement::HttpIntegrations::UpdateService.prepend_mod_with('AlertManagement::HttpIntegrations::UpdateService')
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index 0591376bcdf..605ab7a1869 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -25,13 +25,6 @@ module AlertManagement
attr_reader :project, :payload
- override :process_new_alert
- def process_new_alert
- return if resolving_alert?
-
- super
- end
-
override :incoming_payload
def incoming_payload
strong_memoize(:incoming_payload) do
diff --git a/app/services/analytics/cycle_analytics/stages/base_service.rb b/app/services/analytics/cycle_analytics/stages/base_service.rb
new file mode 100644
index 00000000000..b676eff0a0b
--- /dev/null
+++ b/app/services/analytics/cycle_analytics/stages/base_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module Stages
+ class BaseService
+ include Gitlab::Allowable
+
+ DEFAULT_VALUE_STREAM_NAME = 'default'
+
+ def initialize(parent:, current_user:, params: {})
+ @parent = parent
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ raise NotImplementedError
+ end
+
+ private
+
+ attr_reader :parent, :current_user, :params
+
+ def success(stage, http_status = :created)
+ ServiceResponse.success(payload: { stage: stage }, http_status: http_status)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden)
+ end
+
+ def build_default_stages
+ Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params|
+ parent.cycle_analytics_stages.build(stage_params.merge(value_stream: value_stream))
+ end
+ end
+
+ def value_stream
+ @value_stream ||= params[:value_stream]
+ end
+ end
+ end
+ end
+end
+
+Analytics::CycleAnalytics::Stages::BaseService.prepend_mod_with('Analytics::CycleAnalytics::Stages::BaseService')
diff --git a/app/services/analytics/cycle_analytics/stages/list_service.rb b/app/services/analytics/cycle_analytics/stages/list_service.rb
new file mode 100644
index 00000000000..a6b94ef8295
--- /dev/null
+++ b/app/services/analytics/cycle_analytics/stages/list_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module Stages
+ class ListService < Analytics::CycleAnalytics::Stages::BaseService
+ def execute
+ return forbidden unless allowed?
+
+ success(build_default_stages)
+ end
+
+ private
+
+ def allowed?
+ can?(current_user, :read_cycle_analytics, parent)
+ end
+
+ def success(stages)
+ ServiceResponse.success(payload: { stages: stages })
+ end
+ end
+ end
+ end
+end
+
+Analytics::CycleAnalytics::Stages::ListService.prepend_mod_with('Analytics::CycleAnalytics::Stages::ListService')
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 7792b811b4e..7728982779e 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -120,4 +120,4 @@ module ApplicationSettings
end
end
-ApplicationSettings::UpdateService.prepend_if_ee('EE::ApplicationSettings::UpdateService')
+ApplicationSettings::UpdateService.prepend_mod_with('ApplicationSettings::UpdateService')
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index 92500fbc254..96cde9057c7 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -25,4 +25,4 @@ module Applications
end
end
-Applications::CreateService.prepend_if_ee('EE::Applications::CreateService')
+Applications::CreateService.prepend_mod_with('Applications::CreateService')
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index d1558c60c3d..60421f61007 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -131,9 +131,9 @@ class AuditEventService
def save_or_track(event)
event.save!
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, audit_event_type: event.class.to_s)
end
end
-AuditEventService.prepend_if_ee('EE::AuditEventService')
+AuditEventService.prepend_mod_with('AuditEventService')
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index d74f20511bd..5fde346c4ab 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -251,4 +251,4 @@ module Auth
end
end
-Auth::ContainerRegistryAuthenticationService.prepend_if_ee('EE::Auth::ContainerRegistryAuthenticationService')
+Auth::ContainerRegistryAuthenticationService.prepend_mod_with('Auth::ContainerRegistryAuthenticationService')
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 41236286d23..142eebca2e3 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -15,7 +15,7 @@ module AutoMerge
AutoMergeProcessWorker.perform_async(merge_request.id)
strategy.to_sym
- rescue => e
+ rescue StandardError => e
track_exception(e, merge_request)
:failed
end
@@ -35,7 +35,7 @@ module AutoMerge
end
success
- rescue => e
+ rescue StandardError => e
track_exception(e, merge_request)
error("Can't cancel the automatic merge", 406)
end
@@ -47,7 +47,7 @@ module AutoMerge
end
success
- rescue => e
+ rescue StandardError => e
track_exception(e, merge_request)
error("Can't abort the automatic merge", 406)
end
diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb
index c5cbcc7c93b..912248d3a06 100644
--- a/app/services/auto_merge_service.rb
+++ b/app/services/auto_merge_service.rb
@@ -74,4 +74,4 @@ class AutoMergeService < BaseService
end
end
-AutoMergeService.prepend_if_ee('EE::AutoMergeService')
+AutoMergeService.prepend_mod_with('AutoMergeService')
diff --git a/app/services/award_emojis/add_service.rb b/app/services/award_emojis/add_service.rb
index ceb7a38cead..f45a4330c09 100644
--- a/app/services/award_emojis/add_service.rb
+++ b/app/services/award_emojis/add_service.rb
@@ -46,4 +46,4 @@ module AwardEmojis
end
end
-AwardEmojis::AddService.prepend_if_ee('EE::AwardEmojis::AddService')
+AwardEmojis::AddService.prepend_mod_with('AwardEmojis::AddService')
diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb
index cfd194262f9..47dc8418e07 100644
--- a/app/services/award_emojis/destroy_service.rb
+++ b/app/services/award_emojis/destroy_service.rb
@@ -26,4 +26,4 @@ module AwardEmojis
end
end
-AwardEmojis::DestroyService.prepend_if_ee('EE::AwardEmojis::DestroyService')
+AwardEmojis::DestroyService.prepend_mod_with('AwardEmojis::DestroyService')
diff --git a/app/services/base_container_service.rb b/app/services/base_container_service.rb
index 6852237dc25..ee15763ce65 100644
--- a/app/services/base_container_service.rb
+++ b/app/services/base_container_service.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
-# Base class, scoped by container (project or group)
+# Base class, scoped by container (project or group).
+#
+# New or existing services which only require project as a container
+# should subclass BaseProjectService.
+#
+# If you require a different but specific, non-polymorphic container (such
+# as group), consider creating a new subclass such as BaseGroupService,
+# and update the related comment at the top of the original BaseService.
class BaseContainerService
include BaseServiceUtility
diff --git a/app/services/base_count_service.rb b/app/services/base_count_service.rb
index 2936bdae16e..c316c488148 100644
--- a/app/services/base_count_service.rb
+++ b/app/services/base_count_service.rb
@@ -49,4 +49,4 @@ class BaseCountService
end
end
-BaseCountService.prepend_if_ee('EE::BaseCountService')
+BaseCountService.prepend_mod_with('BaseCountService')
diff --git a/app/services/base_project_service.rb b/app/services/base_project_service.rb
new file mode 100644
index 00000000000..fb466e61673
--- /dev/null
+++ b/app/services/base_project_service.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Base class, scoped by project
+class BaseProjectService < ::BaseContainerService
+ attr_accessor :project
+
+ def initialize(project:, current_user: nil, params: {})
+ super(container: project, current_user: current_user, params: params)
+
+ @project = project
+ end
+
+ delegate :repository, to: :project
+end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 20dfeb67815..7ab87a1af09 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -6,9 +6,12 @@
# and existing service will use these one by one.
# After all are migrated, we can remove this class.
#
-# TODO: New services should consider inheriting from
-# BaseContainerService, or create new base class:
-# https://gitlab.com/gitlab-org/gitlab/-/issues/216672
+# New services should consider inheriting from:
+#
+# - BaseContainerService for services scoped by container (project or group)
+# - BaseProjectService for services scoped to projects
+#
+# or, create a new base class and update this comment.
class BaseService
include BaseServiceUtility
diff --git a/app/services/boards/base_items_list_service.rb b/app/services/boards/base_items_list_service.rb
index 5aebf216460..cbc7a332cbe 100644
--- a/app/services/boards/base_items_list_service.rb
+++ b/app/services/boards/base_items_list_service.rb
@@ -129,7 +129,7 @@ module Boards
# rubocop: disable CodeReuse/ActiveRecord
def label_links(label_ids)
LabelLink
- .where('label_links.target_type = ?', item_model)
+ .where(label_links: { target_type: item_model })
.where(item_model.arel_table[:id].eq(LabelLink.arel_table[:target_id]).to_sql)
.where(label_id: label_ids)
end
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
index 83bb69b3822..f371f88d44b 100644
--- a/app/services/boards/base_service.rb
+++ b/app/services/boards/base_service.rb
@@ -13,4 +13,4 @@ module Boards
end
end
-Boards::BaseService.prepend_if_ee('EE::Boards::BaseService')
+Boards::BaseService.prepend_mod_with('Boards::BaseService')
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 54dab581686..5f014abe071 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -37,4 +37,4 @@ module Boards
end
end
-Boards::CreateService.prepend_if_ee('EE::Boards::CreateService')
+Boards::CreateService.prepend_mod_with('Boards::CreateService')
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
index 1769966a049..0639acfb399 100644
--- a/app/services/boards/issues/create_service.rb
+++ b/app/services/boards/issues/create_service.rb
@@ -30,10 +30,10 @@ module Boards
end
def create_issue(params)
- ::Issues::CreateService.new(project, current_user, params).execute
+ ::Issues::CreateService.new(project: project, current_user: current_user, params: params).execute
end
end
end
end
-Boards::Issues::CreateService.prepend_if_ee('EE::Boards::Issues::CreateService')
+Boards::Issues::CreateService.prepend_mod_with('Boards::Issues::CreateService')
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index c6855f29af0..6284e454561 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -50,4 +50,4 @@ module Boards
end
end
-Boards::Issues::ListService.prepend_if_ee('EE::Boards::Issues::ListService')
+Boards::Issues::ListService.prepend_mod_with('Boards::Issues::ListService')
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 76ea57968b2..959a7fa3ad2 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -52,7 +52,7 @@ module Boards
end
def update(issue, issue_modification_params)
- ::Issues::UpdateService.new(issue.project, current_user, issue_modification_params).execute(issue)
+ ::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: issue_modification_params).execute(issue)
end
def reposition_parent
@@ -62,4 +62,4 @@ module Boards
end
end
-Boards::Issues::MoveService.prepend_if_ee('EE::Boards::Issues::MoveService')
+Boards::Issues::MoveService.prepend_mod_with('Boards::Issues::MoveService')
diff --git a/app/services/boards/lists/base_destroy_service.rb b/app/services/boards/lists/base_destroy_service.rb
new file mode 100644
index 00000000000..dc0247c40b0
--- /dev/null
+++ b/app/services/boards/lists/base_destroy_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Boards
+ module Lists
+ # This class is used by issue and epic board lists
+ # for destroying a single list
+ class BaseDestroyService < Boards::BaseService
+ def execute(list)
+ unless list.destroyable?
+ return ServiceResponse.error(message: "Open and closed lists on a board cannot be destroyed.")
+ end
+
+ list.with_lock do
+ decrement_higher_lists(list)
+ list.destroy!
+ end
+
+ ServiceResponse.success
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
+ ServiceResponse.error(message: "List destroy failed.")
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def decrement_higher_lists(list)
+ list.board.lists.movable.where('position > ?', list.position)
+ .update_all('position = position - 1')
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/app/services/boards/lists/base_update_service.rb b/app/services/boards/lists/base_update_service.rb
index faf58e405fc..bcb7d6c8504 100644
--- a/app/services/boards/lists/base_update_service.rb
+++ b/app/services/boards/lists/base_update_service.rb
@@ -3,16 +3,30 @@
module Boards
module Lists
class BaseUpdateService < Boards::BaseService
+ extend ::Gitlab::Utils::Override
+
def execute(list)
if execute_by_params(list)
success(list: list)
else
- error(list.errors.messages, 422)
+ message = list.errors.empty? ? 'The update was not successful.' : list.errors.messages
+
+ error(message, { list: list })
end
end
private
+ override :error
+ def error(message, pass_back = {})
+ ServiceResponse.error(message: message, http_status: :unprocessable_entity, payload: pass_back)
+ end
+
+ override :success
+ def success(pass_back = {})
+ ServiceResponse.success(payload: pass_back)
+ end
+
def execute_by_params(list)
update_preferences_result = update_preferences(list) if can_read?(list)
update_position_result = update_position(list) if can_admin?(list)
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 37fe0a815bd..3ee0b6d8821 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -7,4 +7,4 @@ module Boards
end
end
-Boards::Lists::CreateService.prepend_if_ee('EE::Boards::Lists::CreateService')
+Boards::Lists::CreateService.prepend_mod_with('Boards::Lists::CreateService')
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index ebac0f07fe1..10d9f275d3f 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -2,36 +2,8 @@
module Boards
module Lists
- class DestroyService < Boards::BaseService
- def execute(list)
- unless list.destroyable?
- return ServiceResponse.error(message: "The list cannot be destroyed. Only label lists can be destroyed.")
- end
-
- @board = list.board
-
- list.with_lock do
- decrement_higher_lists(list)
- remove_list(list)
- end
-
- ServiceResponse.success
- end
-
- private
-
- attr_reader :board
-
- # rubocop: disable CodeReuse/ActiveRecord
- def decrement_higher_lists(list)
- board.lists.movable.where('position > ?', list.position)
- .update_all('position = position - 1')
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def remove_list(list)
- list.destroy!
- end
+ # overridden in EE for board lists and also for epic board lists.
+ class DestroyService < Boards::Lists::BaseDestroyService
end
end
end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index 03d54a8c74c..e81ef467a4e 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -32,4 +32,4 @@ module Boards
end
end
-Boards::Lists::ListService.prepend_if_ee('EE::Boards::Lists::ListService')
+Boards::Lists::ListService.prepend_mod_with('Boards::Lists::ListService')
diff --git a/app/services/boards/lists/update_service.rb b/app/services/boards/lists/update_service.rb
index 2e1a6592cd9..5c24c0daa73 100644
--- a/app/services/boards/lists/update_service.rb
+++ b/app/services/boards/lists/update_service.rb
@@ -14,4 +14,4 @@ module Boards
end
end
-Boards::Lists::UpdateService.prepend_if_ee('EE::Boards::Lists::UpdateService')
+Boards::Lists::UpdateService.prepend_mod_with('Boards::Lists::UpdateService')
diff --git a/app/services/boards/update_service.rb b/app/services/boards/update_service.rb
index 48c6e44d55e..6ba8f68a4cb 100644
--- a/app/services/boards/update_service.rb
+++ b/app/services/boards/update_service.rb
@@ -19,4 +19,4 @@ module Boards
end
end
-Boards::UpdateService.prepend_if_ee('EE::Boards::UpdateService')
+Boards::UpdateService.prepend_mod_with('Boards::UpdateService')
diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
index 428ed1a8bcc..4d659596803 100644
--- a/app/services/boards/visits/create_service.rb
+++ b/app/services/boards/visits/create_service.rb
@@ -5,13 +5,17 @@ module Boards
class CreateService < Boards::BaseService
def execute(board)
return unless current_user && Gitlab::Database.read_write?
- return unless board.is_a?(Board) # other board types do not support board visits yet
+ return unless board
- if parent.is_a?(Group)
- BoardGroupRecentVisit.visited!(current_user, board)
- else
- BoardProjectRecentVisit.visited!(current_user, board)
- end
+ model.visited!(current_user, board)
+ end
+
+ private
+
+ def model
+ return BoardGroupRecentVisit if parent.is_a?(Group)
+
+ BoardProjectRecentVisit
end
end
end
diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb
index ae756d0856e..adb989be218 100644
--- a/app/services/bulk_create_integration_service.rb
+++ b/app/services/bulk_create_integration_service.rb
@@ -10,7 +10,7 @@ class BulkCreateIntegrationService
def execute
service_list = ServiceList.new(batch, service_hash, association).to_array
- Service.transaction do
+ Integration.transaction do
results = bulk_insert(*service_list)
if integration.data_fields_present?
diff --git a/app/services/bulk_imports/export_service.rb b/app/services/bulk_imports/export_service.rb
new file mode 100644
index 00000000000..33b3a8e187f
--- /dev/null
+++ b/app/services/bulk_imports/export_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportService
+ def initialize(portable:, user:)
+ @portable = portable
+ @current_user = user
+ end
+
+ def execute
+ FileTransfer.config_for(portable).portable_relations.each do |relation|
+ RelationExportWorker.perform_async(current_user.id, portable.id, portable.class.name, relation)
+ end
+
+ ServiceResponse.success
+ rescue StandardError => e
+ ServiceResponse.error(
+ message: e.class,
+ http_status: :unprocessable_entity
+ )
+ end
+
+ private
+
+ attr_reader :portable, :current_user
+ end
+end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
new file mode 100644
index 00000000000..53952a33b5f
--- /dev/null
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class RelationExportService
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(user, portable, relation, jid)
+ @user = user
+ @portable = portable
+ @relation = relation
+ @jid = jid
+ end
+
+ def execute
+ find_or_create_export! do |export|
+ remove_existing_export_file!(export)
+ serialize_relation_to_file(export.relation_definition)
+ compress_exported_relation
+ upload_compressed_file(export)
+ end
+ end
+
+ private
+
+ attr_reader :user, :portable, :relation, :jid
+
+ def find_or_create_export!
+ validate_user_permissions!
+
+ export = portable.bulk_import_exports.safe_find_or_create_by!(relation: relation)
+ export.update!(status_event: 'start', jid: jid)
+
+ yield export
+
+ export.update!(status_event: 'finish', error: nil)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, portable_id: portable.id, portable_type: portable.class.name)
+
+ export&.update(status_event: 'fail_op', error: e.class)
+ end
+
+ def validate_user_permissions!
+ ability = "admin_#{portable.to_ability_name}"
+
+ user.can?(ability, portable) ||
+ raise(::Gitlab::ImportExport::Error.permission_error(user, portable))
+ end
+
+ def remove_existing_export_file!(export)
+ upload = export.upload
+
+ return unless upload&.export_file&.file
+
+ upload.remove_export_file!
+ upload.save!
+ end
+
+ def serialize_relation_to_file(relation_definition)
+ serializer.serialize_relation(relation_definition)
+ end
+
+ def compress_exported_relation
+ gzip(dir: export_path, filename: ndjson_filename)
+ end
+
+ def upload_compressed_file(export)
+ compressed_filename = File.join(export_path, "#{ndjson_filename}.gz")
+ upload = ExportUpload.find_or_initialize_by(export_id: export.id) # rubocop: disable CodeReuse/ActiveRecord
+
+ File.open(compressed_filename) { |file| upload.export_file = file }
+
+ upload.save!
+ end
+
+ def config
+ @config ||= FileTransfer.config_for(portable)
+ end
+
+ def export_path
+ @export_path ||= config.export_path
+ end
+
+ def portable_tree
+ @portable_tree ||= config.portable_tree
+ end
+
+ # rubocop: disable CodeReuse/Serializer
+ def serializer
+ @serializer ||= ::Gitlab::ImportExport::JSON::StreamingSerializer.new(
+ portable,
+ portable_tree,
+ json_writer,
+ exportable_path: ''
+ )
+ end
+ # rubocop: enable CodeReuse/Serializer
+
+ def json_writer
+ @json_writer ||= ::Gitlab::ImportExport::JSON::NdjsonWriter.new(export_path)
+ end
+
+ def ndjson_filename
+ @ndjson_filename ||= "#{relation}.ndjson"
+ end
+ end
+end
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
index 5ddfdd359c2..29cfd824c12 100644
--- a/app/services/bulk_update_integration_service.rb
+++ b/app/services/bulk_update_integration_service.rb
@@ -8,8 +8,8 @@ class BulkUpdateIntegrationService
# rubocop: disable CodeReuse/ActiveRecord
def execute
- Service.transaction do
- Service.where(id: batch.select(:id)).update_all(service_hash)
+ Integration.transaction do
+ Integration.where(id: batch.select(:id)).update_all(service_hash)
if integration.data_fields_present?
integration.data_fields.class.where(service_id: batch.select(:id)).update_all(data_fields_hash)
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index c91738fa4c7..3dd3ba7f01c 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -2,8 +2,8 @@
module ChatNames
class FindUserService
- def initialize(service, params)
- @service = service
+ def initialize(integration, params)
+ @integration = integration
@params = params
end
@@ -20,7 +20,7 @@ module ChatNames
# rubocop: disable CodeReuse/ActiveRecord
def find_chat_name
ChatName.find_by(
- service: @service,
+ integration: @integration,
team_id: @params[:team_id],
chat_id: @params[:user_id]
)
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb
index 3858ee9d550..2b611c857c7 100644
--- a/app/services/ci/after_requeue_job_service.rb
+++ b/app/services/ci/after_requeue_job_service.rb
@@ -4,7 +4,7 @@ module Ci
class AfterRequeueJobService < ::BaseService
def execute(processable)
process_subsequent_jobs(processable)
- reset_ancestor_bridges(processable)
+ reset_source_bridge(processable)
end
private
@@ -15,8 +15,8 @@ module Ci
end
end
- def reset_ancestor_bridges(processable)
- processable.pipeline.reset_ancestor_bridges!
+ def reset_source_bridge(processable)
+ processable.pipeline.reset_source_bridge!(current_user)
end
def process(processable)
diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb
index 9b2c7788897..bc3219fbd79 100644
--- a/app/services/ci/archive_trace_service.rb
+++ b/app/services/ci/archive_trace_service.rb
@@ -24,7 +24,7 @@ module Ci
end
rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
# It's already archived, thus we can safely ignore this exception.
- rescue => e
+ rescue StandardError => e
# Tracks this error with application logs, Sentry, and Prometheus.
# If `archive!` keeps failing for over a week, that could incur data loss.
# (See more https://docs.gitlab.com/ee/administration/job_logs.html#new-incremental-logging-architecture)
diff --git a/app/services/ci/change_variable_service.rb b/app/services/ci/change_variable_service.rb
index f515a335d54..83cd6aae14b 100644
--- a/app/services/ci/change_variable_service.rb
+++ b/app/services/ci/change_variable_service.rb
@@ -30,4 +30,4 @@ module Ci
end
end
-::Ci::ChangeVariableService.prepend_if_ee('EE::Ci::ChangeVariableService')
+::Ci::ChangeVariableService.prepend_mod_with('Ci::ChangeVariableService')
diff --git a/app/services/ci/change_variables_service.rb b/app/services/ci/change_variables_service.rb
index 3337eb09411..7a68bd2e2b3 100644
--- a/app/services/ci/change_variables_service.rb
+++ b/app/services/ci/change_variables_service.rb
@@ -8,4 +8,4 @@ module Ci
end
end
-::Ci::ChangeVariablesService.prepend_if_ee('EE::Ci::ChangeVariablesService')
+::Ci::ChangeVariablesService.prepend_mod_with('Ci::ChangeVariablesService')
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index 93f0338fcba..64a99e404c6 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -85,6 +85,12 @@ module Ci
return false
end
+ if has_cyclic_dependency?
+ @bridge.drop!(:pipeline_loop_detected)
+
+ return false
+ end
+
true
end
@@ -109,11 +115,24 @@ module Ci
end
end
+ def has_cyclic_dependency?
+ return false if @bridge.triggers_child_pipeline?
+
+ if Feature.enabled?(:ci_drop_cyclical_triggered_pipelines, @bridge.project, default_enabled: :yaml)
+ checksums = @bridge.pipeline.base_and_ancestors.map { |pipeline| config_checksum(pipeline) }
+ checksums.uniq.length != checksums.length
+ end
+ end
+
def has_max_descendants_depth?
return false unless @bridge.triggers_child_pipeline?
ancestors_of_new_child = @bridge.pipeline.base_and_ancestors(same_project: true)
ancestors_of_new_child.count > MAX_DESCENDANTS_DEPTH
end
+
+ def config_checksum(pipeline)
+ [pipeline.project_id, pipeline.ref].hash
+ end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index ca936307acc..fd333e24860 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -10,6 +10,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Build::Associations,
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
+ Gitlab::Ci::Pipeline::Chain::Validate::SecurityOrchestrationPolicy,
Gitlab::Ci::Pipeline::Chain::Config::Content,
Gitlab::Ci::Pipeline::Chain::Config::Process,
Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
@@ -84,7 +85,6 @@ module Ci
if pipeline.persisted?
schedule_head_pipeline_update
- record_conversion_event
create_namespace_onboarding_action
end
@@ -122,12 +122,6 @@ module Ci
end
end
- def record_conversion_event
- return unless project.namespace.recent?
-
- Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates_b, current_user.id)
- end
-
def create_namespace_onboarding_action
Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id)
end
@@ -138,4 +132,4 @@ module Ci
end
end
-Ci::CreatePipelineService.prepend_if_ee('EE::Ci::CreatePipelineService')
+Ci::CreatePipelineService.prepend_mod_with('Ci::CreatePipelineService')
diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb
index 3b89a599180..db8f61c81fa 100644
--- a/app/services/ci/create_web_ide_terminal_service.rb
+++ b/app/services/ci/create_web_ide_terminal_service.rb
@@ -28,6 +28,11 @@ module Ci
def create_pipeline!
build_pipeline.tap do |pipeline|
pipeline.stages << terminal_stage_seed(pipeline).to_resource
+
+ # Project iid must be called outside a transaction, so we ensure it is set here
+ # otherwise it may be set within the save! which it will lock the InternalId row for the whole transaction
+ pipeline.ensure_project_iid!
+
pipeline.save!
Ci::ProcessPipelineService
diff --git a/app/services/ci/delete_unit_tests_service.rb b/app/services/ci/delete_unit_tests_service.rb
new file mode 100644
index 00000000000..28f96351175
--- /dev/null
+++ b/app/services/ci/delete_unit_tests_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ci
+ class DeleteUnitTestsService
+ include EachBatch
+
+ BATCH_SIZE = 100
+
+ def execute
+ purge_data!(Ci::UnitTestFailure)
+ purge_data!(Ci::UnitTest)
+ end
+
+ private
+
+ def purge_data!(klass)
+ loop do
+ break unless delete_batch!(klass)
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def delete_batch!(klass)
+ deleted = 0
+
+ ActiveRecord::Base.transaction do
+ ids = klass.deletable.lock('FOR UPDATE SKIP LOCKED').limit(BATCH_SIZE).pluck(:id)
+ break if ids.empty?
+
+ deleted = klass.where(id: ids).delete_all
+ end
+
+ deleted > 0
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
index 2ae60907dab..80c83818d0b 100644
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ b/app/services/ci/expire_pipeline_cache_service.rb
@@ -56,6 +56,10 @@ module Ci
url_helpers.graphql_etag_pipeline_path(pipeline)
end
+ def graphql_pipeline_sha_path(sha)
+ url_helpers.graphql_etag_pipeline_sha_path(sha)
+ end
+
# Updates ETag caches of a pipeline.
#
# This logic resides in a separate method so that EE can more easily extend
@@ -76,6 +80,7 @@ module Ci
pipeline.self_with_ancestors_and_descendants.each do |relative_pipeline|
store.touch(project_pipeline_path(relative_pipeline.project, relative_pipeline))
store.touch(graphql_pipeline_path(relative_pipeline))
+ store.touch(graphql_pipeline_sha_path(relative_pipeline.sha))
end
end
diff --git a/app/services/ci/generate_codequality_mr_diff_report_service.rb b/app/services/ci/generate_codequality_mr_diff_report_service.rb
index 3b1bd319a4f..117b0a21eaa 100644
--- a/app/services/ci/generate_codequality_mr_diff_report_service.rb
+++ b/app/services/ci/generate_codequality_mr_diff_report_service.rb
@@ -12,9 +12,9 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
- data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request.new_paths)
+ data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_quality_mr_diff).present.for_files(merge_request)
}
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
{
status: :error,
diff --git a/app/services/ci/generate_coverage_reports_service.rb b/app/services/ci/generate_coverage_reports_service.rb
index 4e6fbc5462a..12b1f19f4b5 100644
--- a/app/services/ci/generate_coverage_reports_service.rb
+++ b/app/services/ci/generate_coverage_reports_service.rb
@@ -14,7 +14,7 @@ module Ci
key: key(base_pipeline, head_pipeline),
data: head_pipeline.pipeline_artifacts.find_by_file_type(:code_coverage).present.for_files(merge_request.new_paths)
}
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(
e,
project_id: project.id,
diff --git a/app/services/ci/generate_exposed_artifacts_report_service.rb b/app/services/ci/generate_exposed_artifacts_report_service.rb
index 1dbcd192279..dfa7cbd7d98 100644
--- a/app/services/ci/generate_exposed_artifacts_report_service.rb
+++ b/app/services/ci/generate_exposed_artifacts_report_service.rb
@@ -14,7 +14,7 @@ module Ci
key: key(base_pipeline, head_pipeline),
data: data
}
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
{
status: :error,
diff --git a/app/services/ci/generate_terraform_reports_service.rb b/app/services/ci/generate_terraform_reports_service.rb
index d768ce777d4..0ffb2d7e34a 100644
--- a/app/services/ci/generate_terraform_reports_service.rb
+++ b/app/services/ci/generate_terraform_reports_service.rb
@@ -13,7 +13,7 @@ module Ci
key: key(base_pipeline, head_pipeline),
data: head_pipeline.terraform_reports.plans
}
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
{
status: :error,
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 65752e56c64..a22ac87f660 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -136,7 +136,7 @@ module Ci
rescue *OBJECT_STORAGE_ERRORS => error
track_exception(error, params)
error(error.message, :service_unavailable)
- rescue => error
+ rescue StandardError => error
track_exception(error, params)
error(error.message, :bad_request)
end
diff --git a/app/services/ci/job_artifacts/destroy_associations_service.rb b/app/services/ci/job_artifacts/destroy_associations_service.rb
new file mode 100644
index 00000000000..794d24eadf2
--- /dev/null
+++ b/app/services/ci/job_artifacts/destroy_associations_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Ci
+ module JobArtifacts
+ class DestroyAssociationsService
+ BATCH_SIZE = 100
+
+ def initialize(job_artifacts_relation)
+ @job_artifacts_relation = job_artifacts_relation
+ @statistics = {}
+ end
+
+ def destroy_records
+ @job_artifacts_relation.each_batch(of: BATCH_SIZE) do |relation|
+ service = Ci::JobArtifacts::DestroyBatchService.new(relation, pick_up_at: Time.current)
+ result = service.execute(update_stats: false)
+ updates = result[:statistics_updates]
+
+ @statistics.merge!(updates) { |_key, oldval, newval| newval + oldval }
+ end
+ end
+
+ def update_statistics
+ @statistics.each do |project, delta|
+ project.increment_statistic_value(Ci::JobArtifact.project_statistics_name, delta)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/job_artifacts/destroy_batch_service.rb b/app/services/ci/job_artifacts/destroy_batch_service.rb
index 95315dd11ec..8536b88ccc0 100644
--- a/app/services/ci/job_artifacts/destroy_batch_service.rb
+++ b/app/services/ci/job_artifacts/destroy_batch_service.rb
@@ -23,8 +23,8 @@ module Ci
end
# rubocop: disable CodeReuse/ActiveRecord
- def execute
- return success(destroyed_artifacts_count: artifacts_count) if @job_artifacts.empty?
+ def execute(update_stats: true)
+ return success(destroyed_artifacts_count: 0, statistics_updates: {}) if @job_artifacts.empty?
Ci::DeletedObject.transaction do
Ci::DeletedObject.bulk_import(@job_artifacts, @pick_up_at)
@@ -33,10 +33,11 @@ module Ci
end
# This is executed outside of the transaction because it depends on Redis
- update_project_statistics
+ update_project_statistics! if update_stats
increment_monitoring_statistics(artifacts_count)
- success(destroyed_artifacts_count: artifacts_count)
+ success(destroyed_artifacts_count: artifacts_count,
+ statistics_updates: affected_project_statistics)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -45,12 +46,20 @@ module Ci
# This method is implemented in EE and it must do only database work
def destroy_related_records(artifacts); end
- def update_project_statistics
- artifacts_by_project = @job_artifacts.group_by(&:project)
- artifacts_by_project.each do |project, artifacts|
- delta = -artifacts.sum { |artifact| artifact.size.to_i }
- ProjectStatistics.increment_statistic(
- project, Ci::JobArtifact.project_statistics_name, delta)
+ # using ! here since this can't be called inside a transaction
+ def update_project_statistics!
+ affected_project_statistics.each do |project, delta|
+ project.increment_statistic_value(Ci::JobArtifact.project_statistics_name, delta)
+ end
+ end
+
+ def affected_project_statistics
+ strong_memoize(:affected_project_statistics) do
+ artifacts_by_project = @job_artifacts.group_by(&:project)
+ artifacts_by_project.each.with_object({}) do |(project, artifacts), accumulator|
+ delta = -artifacts.sum { |artifact| artifact.size.to_i }
+ accumulator[project] = delta
+ end
end
end
@@ -71,4 +80,4 @@ module Ci
end
end
-Ci::JobArtifacts::DestroyBatchService.prepend_if_ee('EE::Ci::JobArtifacts::DestroyBatchService')
+Ci::JobArtifacts::DestroyBatchService.prepend_mod_with('Ci::JobArtifacts::DestroyBatchService')
diff --git a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
index 5c52eef7ba6..d6865efac9f 100644
--- a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
@@ -2,11 +2,18 @@
module Ci
module PipelineArtifacts
class CreateCodeQualityMrDiffReportService
- def execute(pipeline)
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def execute
return unless pipeline.can_generate_codequality_reports?
return if pipeline.has_codequality_mr_diff_report?
+ return unless new_errors_introduced?
- file = build_carrierwave_file(pipeline)
+ file = build_carrierwave_file!
pipeline.pipeline_artifacts.create!(
project_id: pipeline.project_id,
@@ -20,18 +27,54 @@ module Ci
private
- def build_carrierwave_file(pipeline)
+ attr_reader :pipeline
+
+ def merge_requests
+ strong_memoize(:merge_requests) do
+ pipeline.merge_requests_as_head_pipeline
+ end
+ end
+
+ def head_report
+ strong_memoize(:head_report) do
+ pipeline.codequality_reports
+ end
+ end
+
+ def base_report(merge_request)
+ strong_memoize(:base_report) do
+ merge_request&.base_pipeline&.codequality_reports
+ end
+ end
+
+ def mr_diff_report_by_merge_requests
+ strong_memoize(:mr_diff_report_by_merge_requests) do
+ merge_requests.each_with_object({}) do |merge_request, hash|
+ key = "merge_request_#{merge_request.id}"
+ new_errors = Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report(merge_request), head_report).new_errors
+ next if new_errors.empty?
+
+ hash[key] = Gitlab::Ci::Reports::CodequalityMrDiff.new(new_errors)
+ end
+ end
+ end
+
+ def new_errors_introduced?
+ mr_diff_report_by_merge_requests.present?
+ end
+
+ def build_carrierwave_file!
CarrierWaveStringFile.new_file(
- file_content: build_quality_mr_diff_report(pipeline),
+ file_content: build_quality_mr_diff_report(mr_diff_report_by_merge_requests),
filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
content_type: 'application/json'
)
end
- def build_quality_mr_diff_report(pipeline)
- mr_diff_report = Gitlab::Ci::Reports::CodequalityMrDiff.new(pipeline.codequality_reports)
-
- Ci::CodequalityMrDiffReportSerializer.new.represent(mr_diff_report).to_json # rubocop: disable CodeReuse/Serializer
+ def build_quality_mr_diff_report(mr_diff_report)
+ mr_diff_report.each_with_object({}) do |diff_report, hash|
+ hash[diff_report.first] = Ci::CodequalityMrDiffReportSerializer.new.represent(diff_report.second) # rubocop: disable CodeReuse/Serializer
+ end.to_json
end
end
end
diff --git a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
index fed40aef697..7b6590a117c 100644
--- a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
@@ -25,7 +25,7 @@ module Ci
private
def destroy_artifacts_batch
- artifacts = ::Ci::PipelineArtifact.expired(BATCH_SIZE).to_a
+ artifacts = ::Ci::PipelineArtifact.unlocked.expired(BATCH_SIZE).to_a
return false if artifacts.empty?
artifacts.each(&:destroy!)
diff --git a/app/services/ci/pipeline_bridge_status_service.rb b/app/services/ci/pipeline_bridge_status_service.rb
index e2e5dd386f2..aeac43588f7 100644
--- a/app/services/ci/pipeline_bridge_status_service.rb
+++ b/app/services/ci/pipeline_bridge_status_service.rb
@@ -17,4 +17,4 @@ module Ci
end
end
-Ci::PipelineBridgeStatusService.prepend_if_ee('EE::Ci::PipelineBridgeStatusService')
+Ci::PipelineBridgeStatusService.prepend_mod_with('Ci::PipelineBridgeStatusService')
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index a5f70d62e13..62c4d6b4599 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -3,6 +3,7 @@
module Ci
class PipelineTriggerService < BaseService
include Gitlab::Utils::StrongMemoize
+ include Services::ReturnServiceResponses
def execute
if trigger_from_token
@@ -20,7 +21,7 @@ module Ci
private
PAYLOAD_VARIABLE_KEY = 'TRIGGER_PAYLOAD'
- PAYLOAD_VARIABLE_HIDDEN_PARAMS = %i(token).freeze
+ PAYLOAD_VARIABLE_HIDDEN_PARAMS = %i[token].freeze
def create_pipeline_from_trigger(trigger)
# this check is to not leak the presence of the project if user cannot read it
@@ -32,10 +33,17 @@ module Ci
pipeline.trigger_requests.build(trigger: trigger)
end
- if pipeline.persisted?
+ pipeline_service_response(pipeline)
+ end
+
+ def pipeline_service_response(pipeline)
+ if pipeline.created_successfully?
success(pipeline: pipeline)
+ elsif pipeline.persisted?
+ err = pipeline.errors.messages.presence || pipeline.failure_reason.presence || 'Could not create pipeline'
+ error(err, :unprocessable_entity)
else
- error(pipeline.errors.messages, 400)
+ error(pipeline.errors.messages, :bad_request)
end
end
@@ -61,11 +69,7 @@ module Ci
pipeline.source_pipeline = source
end
- if pipeline.persisted?
- success(pipeline: pipeline)
- else
- error(pipeline.errors.messages, 400)
- end
+ pipeline_service_response(pipeline)
end
def job_from_token
diff --git a/app/services/ci/prepare_build_service.rb b/app/services/ci/prepare_build_service.rb
index 3f87c711270..ec61c43cce9 100644
--- a/app/services/ci/prepare_build_service.rb
+++ b/app/services/ci/prepare_build_service.rb
@@ -12,7 +12,7 @@ module Ci
prerequisites.each(&:complete!)
build.enqueue_preparing!
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, build_id: build.id)
build.drop(:unmet_prerequisites)
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
index 73cf3308fe7..5271c0fe93d 100644
--- a/app/services/ci/process_build_service.rb
+++ b/app/services/ci/process_build_service.rb
@@ -40,4 +40,4 @@ module Ci
end
end
-Ci::ProcessBuildService.prepend_if_ee('EE::Ci::ProcessBuildService')
+Ci::ProcessBuildService.prepend_mod_with('Ci::ProcessBuildService')
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 6c69df0c616..fb26d5d3356 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -48,7 +48,13 @@ module Ci
# This counter is temporary. It will be used to check whether if we still use this method or not
# after setting correct value of `GenericCommitStatus#retried`.
# More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50465#note_491657115
- metrics.legacy_update_jobs_counter.increment if updated_count > 0
+ if updated_count > 0
+ Gitlab::AppJsonLogger.info(event: 'update_retried_is_used',
+ project_id: pipeline.project.id,
+ pipeline_id: pipeline.id)
+
+ metrics.legacy_update_jobs_counter.increment
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/prometheus_metrics/observe_histograms_service.rb b/app/services/ci/prometheus_metrics/observe_histograms_service.rb
index 527d87f19c2..6bd3d2121ba 100644
--- a/app/services/ci/prometheus_metrics/observe_histograms_service.rb
+++ b/app/services/ci/prometheus_metrics/observe_histograms_service.rb
@@ -25,8 +25,6 @@ module Ci
end
def execute
- return ServiceResponse.success(http_status: :accepted) unless enabled?
-
params
.fetch(:histograms, [])
.each(&method(:observe))
@@ -48,10 +46,6 @@ module Ci
.fetch(name) { raise ActiveRecord::RecordNotFound }
.call
end
-
- def enabled?
- ::Feature.enabled?(:ci_accept_frontend_prometheus_metrics, project, default_enabled: :yaml)
- end
end
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 90341b26fd6..461647ffccc 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -169,7 +169,7 @@ module Ci
@metrics.increment_queue_operation(:build_conflict_transition)
Result.new(nil, nil, false)
- rescue => ex
+ rescue StandardError => ex
@metrics.increment_queue_operation(:build_conflict_exception)
# If an error (e.g. GRPC::DeadlineExceeded) occurred constructing
@@ -233,7 +233,7 @@ module Ci
Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'register_job_scheduler_failure') do |subject|
subject.drop!(:scheduler_failure)
end
- rescue => ex
+ rescue StandardError => ex
build.doom!
# This requires extra exception, otherwise we would loose information
@@ -253,17 +253,23 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def builds_for_shared_runner
- new_builds.
+ relation = new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true, pending_delete: false })
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
- .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
+ .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0')
- # Implement fair scheduling
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
- .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
+ if Feature.enabled?(:ci_queueing_disaster_recovery, runner, type: :ops, default_enabled: :yaml)
+ # if disaster recovery is enabled, we fallback to FIFO scheduling
+ relation.order('ci_builds.id ASC')
+ else
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ relation
+ .joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
+ .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -310,4 +316,4 @@ module Ci
end
end
-Ci::RegisterJobService.prepend_if_ee('EE::Ci::RegisterJobService')
+Ci::RegisterJobService.prepend_mod_with('Ci::RegisterJobService')
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index e3de7f43fda..e03f2ae3d52 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -18,16 +18,14 @@ module Ci
AfterRequeueJobService.new(project, current_user).execute(build)
::MergeRequests::AddTodoWhenBuildFailsService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.close(new_build)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def reprocess!(build)
- unless can?(current_user, :update_build, build)
- raise Gitlab::Access::AccessDeniedError
- end
+ check_access!(build)
attributes = self.class.clone_accessors.to_h do |attribute|
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
@@ -52,6 +50,12 @@ module Ci
private
+ def check_access!(build)
+ unless can?(current_user, :update_build, build)
+ raise Gitlab::Access::AccessDeniedError
+ end
+ end
+
def create_build!(attributes)
build = project.builds.new(attributes)
build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build))
@@ -64,4 +68,4 @@ module Ci
end
end
-Ci::RetryBuildService.prepend_if_ee('EE::Ci::RetryBuildService')
+Ci::RetryBuildService.prepend_mod_with('Ci::RetryBuildService')
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index bb8590a769c..5cc6b89bfef 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -26,10 +26,10 @@ module Ci
retry_optimistic_lock(skipped, name: 'ci_retry_pipeline') { |build| build.process(current_user) }
end
- pipeline.reset_ancestor_bridges!
+ pipeline.reset_source_bridge!(current_user)
::MergeRequests::AddTodoWhenBuildFailsService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.close_all(pipeline)
Ci::ProcessPipelineService
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index 81457130fa0..7c9fc44e7f4 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -27,7 +27,7 @@ module Ci
stop_actions.each do |stop_action|
stop_action.play(stop_action.user)
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, deployable_id: stop_action.id)
end
end
@@ -35,7 +35,7 @@ module Ci
private
def environments
- @environments ||= EnvironmentsByDeploymentsFinder
+ @environments ||= Environments::EnvironmentsByDeploymentsFinder
.new(project, current_user, ref: @ref, recently_updated: true)
.execute
end
diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb
index 58bbc716ff0..a3f45c1b9cd 100644
--- a/app/services/ci/test_failure_history_service.rb
+++ b/app/services/ci/test_failure_history_service.rb
@@ -30,7 +30,7 @@ module Ci
end
def should_track_failures?
- return false unless project.default_branch_or_master == pipeline.ref
+ return false unless project.default_branch_or_main == pipeline.ref
# We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get
# 201 total number of builds with the assumption that each job has at least
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 249abd3ff9d..10a12f30956 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -19,9 +19,7 @@ module Clusters
def check_timeout
if timed_out?
- begin
- app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
- end
+ app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
else
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
diff --git a/app/services/clusters/applications/check_upgrade_progress_service.rb b/app/services/clusters/applications/check_upgrade_progress_service.rb
index bc161218618..c4fd234b302 100644
--- a/app/services/clusters/applications/check_upgrade_progress_service.rb
+++ b/app/services/clusters/applications/check_upgrade_progress_service.rb
@@ -51,7 +51,7 @@ module Clusters
def remove_pod
helm_api.delete_pod!(pod_name)
- rescue
+ rescue StandardError
# no-op
end
diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb
index 50c4e26b0d0..d39d63c874f 100644
--- a/app/services/clusters/applications/prometheus_config_service.rb
+++ b/app/services/clusters/applications/prometheus_config_service.rb
@@ -96,8 +96,6 @@ module Clusters
end
def alert_manager_token
- app.generate_alert_manager_token!
-
app.alert_manager_token
end
diff --git a/app/services/clusters/applications/prometheus_update_service.rb b/app/services/clusters/applications/prometheus_update_service.rb
index 437f6ab1202..b8b50f06d72 100644
--- a/app/services/clusters/applications/prometheus_update_service.rb
+++ b/app/services/clusters/applications/prometheus_update_service.rb
@@ -2,6 +2,7 @@
module Clusters
module Applications
+ # Deprecated, to be removed in %14.0 as part of https://gitlab.com/groups/gitlab-org/-/epics/4280
class PrometheusUpdateService < BaseHelmService
attr_accessor :project
@@ -11,6 +12,8 @@ module Clusters
end
def execute
+ raise NotImplementedError, 'Externally installed prometheus should not be modified!' unless app.managed_prometheus?
+
app.make_updating!
helm_api.update(patch_command(values))
diff --git a/app/services/clusters/applications/schedule_update_service.rb b/app/services/clusters/applications/schedule_update_service.rb
index 41718df9a98..4f130f76b87 100644
--- a/app/services/clusters/applications/schedule_update_service.rb
+++ b/app/services/clusters/applications/schedule_update_service.rb
@@ -14,6 +14,7 @@ module Clusters
def execute
return unless application
+ return unless application.managed_prometheus?
if recently_scheduled?
worker_class.perform_in(BACKOFF_DELAY, application.name, application.id, project.id, Time.current)
diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb
index 497e676f549..e38852c7ec7 100644
--- a/app/services/clusters/aws/fetch_credentials_service.rb
+++ b/app/services/clusters/aws/fetch_credentials_service.rb
@@ -14,7 +14,7 @@ module Clusters
end
def execute
- raise MissingRoleError.new('AWS provisioning role not configured') unless provision_role.present?
+ raise MissingRoleError, 'AWS provisioning role not configured' unless provision_role.present?
::Aws::AssumeRoleCredentials.new(
client: client,
@@ -54,7 +54,7 @@ module Clusters
##
# If we haven't created a provider record yet,
- # we restrict ourselves to read only access so
+ # we restrict ourselves to read-only access so
# that we can safely expose credentials to the
# frontend (to be used when populating the
# creation form).
diff --git a/app/services/clusters/integrations/create_service.rb b/app/services/clusters/integrations/create_service.rb
index f9e9dd3e457..142f731a7d3 100644
--- a/app/services/clusters/integrations/create_service.rb
+++ b/app/services/clusters/integrations/create_service.rb
@@ -27,12 +27,15 @@ module Clusters
private
def integration
- case params[:application_type]
- when 'prometheus'
- cluster.find_or_build_integration_prometheus
- else
- raise ArgumentError, "invalid application_type: #{params[:application_type]}"
- end
+ @integration ||= \
+ case params[:application_type]
+ when 'prometheus'
+ cluster.find_or_build_integration_prometheus
+ when 'elastic_stack'
+ cluster.find_or_build_integration_elastic_stack
+ else
+ raise ArgumentError, "invalid application_type: #{params[:application_type]}"
+ end
end
def authorized?
diff --git a/app/services/clusters/management/create_project_service.rb b/app/services/clusters/management/create_project_service.rb
deleted file mode 100644
index 5a0176edd12..00000000000
--- a/app/services/clusters/management/create_project_service.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Management
- class CreateProjectService
- CreateError = Class.new(StandardError)
-
- attr_reader :cluster, :current_user
-
- def initialize(cluster, current_user:)
- @cluster = cluster
- @current_user = current_user
- end
-
- def execute
- return unless management_project_required?
-
- project = create_management_project!
- update_cluster!(project)
- end
-
- private
-
- def management_project_required?
- Feature.enabled?(:auto_create_cluster_management_project) && cluster.management_project.nil?
- end
-
- def project_params
- {
- name: project_name,
- description: project_description,
- namespace_id: namespace.id,
- visibility_level: Gitlab::VisibilityLevel::PRIVATE
- }
- end
-
- def project_name
- "#{cluster.name} Cluster Management"
- end
-
- def project_description
- "This project is automatically generated and will be used to manage your Kubernetes cluster. [More information](#{docs_path})"
- end
-
- def docs_path
- Rails.application.routes.url_helpers.help_page_path('user/clusters/management_project')
- end
-
- def create_management_project!
- ::Projects::CreateService.new(current_user, project_params).execute.tap do |project|
- errors = project.errors.full_messages
-
- if errors.any?
- raise CreateError.new("Failed to create project: #{errors}")
- end
- end
- end
-
- def update_cluster!(project)
- unless cluster.update(management_project: project)
- raise CreateError.new("Failed to update cluster: #{cluster.errors.full_messages}")
- end
- end
-
- def namespace
- case cluster.cluster_type
- when 'project_type'
- cluster.project.namespace
- when 'group_type'
- cluster.group
- when 'instance_type'
- instance_administrators_group
- else
- raise NotImplementedError
- end
- end
-
- def instance_administrators_group
- Gitlab::CurrentSettings.instance_administrators_group ||
- raise(CreateError.new('Instance administrators group not found'))
- end
- end
- end
-end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index a1498da302e..fc18420f6e4 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -113,4 +113,4 @@ module Commits
end
end
-Commits::CreateService.prepend_if_ee('EE::Commits::CreateService')
+Commits::CreateService.prepend_mod_with('Commits::CreateService')
diff --git a/app/services/concerns/alert_management/alert_processing.rb b/app/services/concerns/alert_management/alert_processing.rb
index 7b6f681fe3e..98d255dec27 100644
--- a/app/services/concerns/alert_management/alert_processing.rb
+++ b/app/services/concerns/alert_management/alert_processing.rb
@@ -19,11 +19,7 @@ module AlertManagement
# Updates or creates alert from payload for project
# including system notes
def process_alert
- if alert.persisted?
- process_existing_alert
- else
- process_new_alert
- end
+ alert.persisted? ? process_existing_alert : process_new_alert
end
# Creates or closes issue for alert and notifies stakeholders
@@ -33,22 +29,16 @@ module AlertManagement
end
def process_existing_alert
- if resolving_alert?
- process_resolved_alert
- else
- process_firing_alert
- end
+ resolving_alert? ? process_resolved_alert : process_firing_alert
end
def process_resolved_alert
SystemNoteService.log_resolving_alert(alert, alert_source)
- return unless auto_close_incident?
-
if alert.resolve(incoming_payload.ends_at)
SystemNoteService.change_alert_status(alert, User.alert_bot)
- close_issue(alert.issue)
+ close_issue(alert.issue) if auto_close_incident?
else
logger.warn(
message: 'Unable to update AlertManagement::Alert status to resolved',
@@ -66,7 +56,7 @@ module AlertManagement
return if issue.blank? || issue.closed?
::Issues::CloseService
- .new(project, User.alert_bot)
+ .new(project: project, current_user: User.alert_bot)
.execute(issue, system_note: false)
SystemNoteService.auto_resolve_prometheus_alert(issue, project, User.alert_bot) if issue.reset.closed?
@@ -76,6 +66,8 @@ module AlertManagement
if alert.save
alert.execute_services
SystemNoteService.create_new_alert(alert, alert_source)
+
+ process_resolved_alert if resolving_alert?
else
logger.warn(
message: "Unable to create AlertManagement::Alert from #{alert_source}",
@@ -88,7 +80,7 @@ module AlertManagement
def process_incident_issues
return if alert.issue || alert.resolved?
- ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
+ ::IncidentManagement::ProcessAlertWorkerV2.perform_async(alert.id)
end
def send_alert_email
@@ -128,7 +120,7 @@ module AlertManagement
end
def alert_source
- alert.monitoring_tool
+ incoming_payload.monitoring_tool
end
def logger
@@ -137,4 +129,4 @@ module AlertManagement
end
end
-AlertManagement::AlertProcessing.prepend_ee_mod
+AlertManagement::AlertProcessing.prepend_mod
diff --git a/app/services/concerns/integrations/project_test_data.rb b/app/services/concerns/integrations/project_test_data.rb
index 5968b90f8fe..acaa773fd49 100644
--- a/app/services/concerns/integrations/project_test_data.rb
+++ b/app/services/concerns/integrations/project_test_data.rb
@@ -63,7 +63,7 @@ module Integrations
return { error: s_('TestHooks|Ensure the project has deployments.') } unless deployment.present?
- Gitlab::DataBuilder::Deployment.build(deployment)
+ Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
end
def releases_events_data
diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb
index fcb3022a1dc..ebce8a0667a 100644
--- a/app/services/concerns/measurable.rb
+++ b/app/services/concerns/measurable.rb
@@ -23,7 +23,7 @@
# end
# end
#
-# DummyService.prepend_if_ee('EE::DummyService')
+# DummyService.prepend_mod_with('DummyService')
# DummyService.prepend(Measurable)
# ```
#
diff --git a/app/services/concerns/services/return_service_responses.rb b/app/services/concerns/services/return_service_responses.rb
new file mode 100644
index 00000000000..75432b76033
--- /dev/null
+++ b/app/services/concerns/services/return_service_responses.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Services
+ # adapter for existing services over BaseServiceUtility - add error and
+ # success methods returning ServiceResponse objects
+ module ReturnServiceResponses
+ def error(message, http_status, pass_back: {})
+ ServiceResponse.error(message: message, http_status: http_status, payload: pass_back)
+ end
+
+ def success(payload)
+ ServiceResponse.success(payload: payload)
+ end
+ end
+end
diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb
index 69e5620d986..38a3fc231c6 100644
--- a/app/services/container_expiration_policies/cleanup_service.rb
+++ b/app/services/container_expiration_policies/cleanup_service.rb
@@ -13,13 +13,20 @@ module ContainerExpirationPolicies
def execute
return ServiceResponse.error(message: 'no repository') unless repository
+ unless policy.valid?
+ disable_policy!
+
+ return ServiceResponse.error(message: 'invalid policy')
+ end
+
repository.start_expiration_policy!
+ schedule_next_run_if_needed
begin
service_result = Projects::ContainerRepository::CleanupTagsService
.new(project, nil, policy_params.merge('container_expiration_policy' => true))
.execute(repository)
- rescue
+ rescue StandardError
repository.cleanup_unfinished!
raise
@@ -28,7 +35,6 @@ module ContainerExpirationPolicies
if service_result[:status] == :success
repository.update!(
expiration_policy_cleanup_status: :cleanup_unscheduled,
- expiration_policy_started_at: nil,
expiration_policy_completed_at: Time.zone.now
)
@@ -42,6 +48,27 @@ module ContainerExpirationPolicies
private
+ def schedule_next_run_if_needed
+ return unless Feature.enabled?(:container_registry_expiration_policies_loopless)
+ return if policy.next_run_at.future?
+
+ repos_before_next_run = ::ContainerRepository.for_project_id(policy.project_id)
+ .expiration_policy_started_at_nil_or_before(policy.next_run_at)
+ return if repos_before_next_run.exists?
+
+ policy.schedule_next_run!
+ end
+
+ def disable_policy!
+ policy.disable!
+ repository.cleanup_unscheduled!
+
+ Gitlab::ErrorTracking.log_exception(
+ ::ContainerExpirationPolicyWorker::InvalidPolicyError.new,
+ container_expiration_policy_id: policy.id
+ )
+ end
+
def success(cleanup_status, service_result)
payload = {
cleanup_status: cleanup_status,
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
index 2dac94c7ade..3245e749164 100644
--- a/app/services/deploy_keys/create_service.rb
+++ b/app/services/deploy_keys/create_service.rb
@@ -8,4 +8,4 @@ module DeployKeys
end
end
-DeployKeys::CreateService.prepend_if_ee('::EE::DeployKeys::CreateService')
+DeployKeys::CreateService.prepend_mod_with('DeployKeys::CreateService')
diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb
index 9283a5c1279..100d1267848 100644
--- a/app/services/deployments/older_deployments_drop_service.rb
+++ b/app/services/deployments/older_deployments_drop_service.rb
@@ -15,7 +15,7 @@ module Deployments
Gitlab::OptimisticLocking.retry_lock(older_deployment.deployable, name: 'older_deployments_drop') do |deployable|
deployable.drop(:forward_deployment_failure)
end
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, deployment_id: older_deployment.id)
end
end
diff --git a/app/services/deployments/update_environment_service.rb b/app/services/deployments/update_environment_service.rb
index 98fedb9f699..9e862d6fa52 100644
--- a/app/services/deployments/update_environment_service.rb
+++ b/app/services/deployments/update_environment_service.rb
@@ -77,4 +77,4 @@ module Deployments
end
end
-Deployments::UpdateEnvironmentService.prepend_if_ee('EE::Deployments::UpdateEnvironmentService')
+Deployments::UpdateEnvironmentService.prepend_mod_with('Deployments::UpdateEnvironmentService')
diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb
index c0b32e1e9ae..496103f9e58 100644
--- a/app/services/design_management/copy_design_collection/copy_service.rb
+++ b/app/services/design_management/copy_design_collection/copy_service.rb
@@ -47,7 +47,7 @@ module DesignManagement
end
ServiceResponse.success
- rescue => error
+ rescue StandardError => error
log_exception(error)
target_design_collection.error_copy!
diff --git a/app/services/design_management/delete_designs_service.rb b/app/services/design_management/delete_designs_service.rb
index a90c34d4e34..7f76bcc5626 100644
--- a/app/services/design_management/delete_designs_service.rb
+++ b/app/services/design_management/delete_designs_service.rb
@@ -67,4 +67,4 @@ module DesignManagement
end
end
-DesignManagement::DeleteDesignsService.prepend_if_ee('EE::DesignManagement::DeleteDesignsService')
+DesignManagement::DeleteDesignsService.prepend_mod_with('DesignManagement::DeleteDesignsService')
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
index c26d2e7ab47..44ebd45f76e 100644
--- a/app/services/design_management/save_designs_service.rb
+++ b/app/services/design_management/save_designs_service.rb
@@ -141,4 +141,4 @@ module DesignManagement
end
end
-DesignManagement::SaveDesignsService.prepend_if_ee('EE::DesignManagement::SaveDesignsService')
+DesignManagement::SaveDesignsService.prepend_mod_with('DesignManagement::SaveDesignsService')
diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb
index 91c3cf136a4..3b733023eae 100644
--- a/app/services/discussions/resolve_service.rb
+++ b/app/services/discussions/resolve_service.rb
@@ -44,7 +44,7 @@ module Discussions
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
.track_resolve_thread_action(user: current_user)
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
end
SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index 82917241347..d73c3417a8b 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -24,7 +24,7 @@ module DraftNotes
create_note_from_draft(draft)
draft.delete
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
end
def publish_draft_notes
@@ -41,7 +41,7 @@ module DraftNotes
set_reviewed
notification_service.async.new_review(review)
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+ MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
end
def create_note_from_draft(draft)
@@ -68,7 +68,7 @@ module DraftNotes
end
def set_reviewed
- ::MergeRequests::MarkReviewerReviewedService.new(project, current_user).execute(merge_request)
+ ::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
end
end
end
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index c94505b2068..58fc9799673 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -12,4 +12,4 @@ module Emails
end
end
-Emails::BaseService.prepend_if_ee('::EE::Emails::BaseService')
+Emails::BaseService.prepend_mod_with('Emails::BaseService')
diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb
index 473256d9c6f..011978ba76a 100644
--- a/app/services/emails/create_service.rb
+++ b/app/services/emails/create_service.rb
@@ -12,4 +12,4 @@ module Emails
end
end
-Emails::CreateService.prepend_if_ee('EE::Emails::CreateService')
+Emails::CreateService.prepend_mod_with('Emails::CreateService')
diff --git a/app/services/emails/destroy_service.rb b/app/services/emails/destroy_service.rb
index 6e671f52d57..288bee84ef7 100644
--- a/app/services/emails/destroy_service.rb
+++ b/app/services/emails/destroy_service.rb
@@ -18,4 +18,4 @@ module Emails
end
end
-Emails::DestroyService.prepend_if_ee('EE::Emails::DestroyService')
+Emails::DestroyService.prepend_mod_with('Emails::DestroyService')
diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb
index b8235678d1d..2f8bbfddef0 100644
--- a/app/services/error_tracking/issue_update_service.rb
+++ b/app/services/error_tracking/issue_update_service.rb
@@ -35,7 +35,7 @@ module ErrorTracking
def close_issue(issue)
Issues::CloseService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.execute(issue, system_note: false)
end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 85658598afc..01a40fc6473 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -222,4 +222,4 @@ class EventCreateService
end
end
-EventCreateService.prepend_if_ee('EE::EventCreateService')
+EventCreateService.prepend_mod_with('EventCreateService')
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 825faf59c13..a49b981c680 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -96,7 +96,6 @@ module Git
def track_ci_config_change_event
return unless Gitlab::CurrentSettings.usage_ping_enabled?
- return unless ::Feature.enabled?(:usage_data_unique_users_committing_ciconfigfile, project, default_enabled: :yaml)
return unless default_branch?
commits_changing_ci_config.each do |commit|
@@ -227,4 +226,4 @@ module Git
end
end
-Git::BranchHooksService.prepend_if_ee('::EE::Git::BranchHooksService')
+Git::BranchHooksService.prepend_mod_with('Git::BranchHooksService')
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index d250bca7bf2..5dcc2de456c 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -94,4 +94,4 @@ module Git
end
end
-Git::BranchPushService.prepend_if_ee('::EE::Git::BranchPushService')
+Git::BranchPushService.prepend_mod_with('Git::BranchPushService')
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index d039ed00efc..6f348ff9e0b 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -77,7 +77,7 @@ module Git
def merge_request_branches_for(ref_type, changes)
return [] if ref_type == :tag
- MergeRequests::PushedBranchesService.new(project, current_user, changes: changes).execute
+ MergeRequests::PushedBranchesService.new(project: project, current_user: current_user, params: { changes: changes }).execute
end
end
end
diff --git a/app/services/git/tag_hooks_service.rb b/app/services/git/tag_hooks_service.rb
index 0e5e1bbc992..d83924fec28 100644
--- a/app/services/git/tag_hooks_service.rb
+++ b/app/services/git/tag_hooks_service.rb
@@ -35,4 +35,4 @@ module Git
end
end
-Git::TagHooksService.prepend_if_ee('::EE::Git::TagHooksService')
+Git::TagHooksService.prepend_mod_with('Git::TagHooksService')
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index 82958abfe6e..11aeb650a5b 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -86,4 +86,4 @@ module Git
end
end
-Git::WikiPushService.prepend_if_ee('EE::Git::WikiPushService')
+Git::WikiPushService.prepend_mod_with('Git::WikiPushService')
diff --git a/app/services/groups/autocomplete_service.rb b/app/services/groups/autocomplete_service.rb
new file mode 100644
index 00000000000..92b05d9ac08
--- /dev/null
+++ b/app/services/groups/autocomplete_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Groups
+ class AutocompleteService < Groups::BaseService
+ include LabelsAsHash
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def issues(confidential_only: false, issue_types: nil)
+ finder_params = { group_id: group.id, include_subgroups: true, state: 'opened' }
+ finder_params[:confidential] = true if confidential_only.present?
+ finder_params[:issue_types] = issue_types if issue_types.present?
+
+ IssuesFinder.new(current_user, finder_params)
+ .execute
+ .preload(project: :namespace)
+ .select(:iid, :title, :project_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def merge_requests
+ MergeRequestsFinder.new(current_user, group_id: group.id, include_subgroups: true, state: 'opened')
+ .execute
+ .preload(target_project: :namespace)
+ .select(:iid, :title, :target_project_id)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def milestones
+ group_ids = group.self_and_ancestors.public_or_visible_to_user(current_user).pluck(:id)
+
+ MilestonesFinder.new(group_ids: group_ids).execute.select(:iid, :title, :due_date)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def labels_as_hash(target)
+ super(target, group_id: group.id, only_group_labels: true, include_ancestor_groups: true)
+ end
+
+ def commands(noteable)
+ return [] unless noteable
+
+ QuickActions::InterpretService.new(nil, current_user).available_commands(noteable)
+ end
+ end
+end
+
+Groups::AutocompleteService.prepend_mod
diff --git a/app/services/groups/count_service.rb b/app/services/groups/count_service.rb
index 2a15ae3bc57..735acddb025 100644
--- a/app/services/groups/count_service.rb
+++ b/app/services/groups/count_service.rb
@@ -19,13 +19,26 @@ module Groups
cached_count = Rails.cache.read(cache_key)
return cached_count unless cached_count.blank?
- refreshed_count = uncached_count
- update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD
- refreshed_count
+ refresh_cache_over_threshold
end
- def cache_key
- ['groups', "#{issuable_key}_count_service", VERSION, group.id, cache_key_name]
+ def refresh_cache_over_threshold(key = nil)
+ key ||= cache_key
+ new_count = uncached_count
+
+ if new_count > CACHED_COUNT_THRESHOLD
+ update_cache_for_key(key) { new_count }
+ else
+ delete_cache
+ end
+
+ new_count
+ end
+
+ def cache_key(key_name = nil)
+ key_name ||= cache_key_name
+
+ ['groups', "#{issuable_key}_count_service", VERSION, group.id, key_name]
end
private
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 9ddb8ae7695..8e8efe7d555 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -37,7 +37,7 @@ module Groups
Group.transaction do
if @group.save
@group.add_owner(current_user)
- Service.create_from_active_default_integrations(@group, :group_id)
+ Integration.create_from_active_default_integrations(@group, :group_id)
OnboardingProgress.onboard(@group)
end
end
@@ -103,4 +103,4 @@ module Groups
end
end
-Groups::CreateService.prepend_if_ee('EE::Groups::CreateService')
+Groups::CreateService.prepend_mod_with('Groups::CreateService')
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index a27330d1104..08c4e0231e7 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -58,4 +58,4 @@ module Groups
end
end
-Groups::DestroyService.prepend_if_ee('EE::Groups::DestroyService')
+Groups::DestroyService.prepend_mod_with('Groups::DestroyService')
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index a436aec1b39..ea26ebec20b 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -96,7 +96,7 @@ module Groups
def notify_error!
notify_error
- raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
+ raise Gitlab::ImportExport::Error, shared.errors.to_sentence
end
def notify_success
@@ -127,4 +127,4 @@ module Groups
end
end
-Groups::ImportExport::ExportService.prepend_if_ee('EE::Groups::ImportExport::ExportService')
+Groups::ImportExport::ExportService.prepend_mod_with('Groups::ImportExport::ExportService')
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index bf3f09f22d4..f9db552f743 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -114,7 +114,7 @@ module Groups
def notify_error!
notify_error
- raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
+ raise Gitlab::ImportExport::Error, shared.errors.to_sentence
end
def remove_base_tmp_dir
@@ -124,4 +124,4 @@ module Groups
end
end
-Groups::ImportExport::ImportService.prepend_if_ee('EE::Groups::ImportExport::ImportService')
+Groups::ImportExport::ImportService.prepend_mod_with('Groups::ImportExport::ImportService')
diff --git a/app/services/groups/open_issues_count_service.rb b/app/services/groups/open_issues_count_service.rb
index ef787a04315..17cf3d38987 100644
--- a/app/services/groups/open_issues_count_service.rb
+++ b/app/services/groups/open_issues_count_service.rb
@@ -6,6 +6,12 @@ module Groups
PUBLIC_COUNT_KEY = 'group_public_open_issues_count'
TOTAL_COUNT_KEY = 'group_total_open_issues_count'
+ def clear_all_cache_keys
+ [cache_key(PUBLIC_COUNT_KEY), cache_key(TOTAL_COUNT_KEY)].each do |key|
+ Rails.cache.delete(key)
+ end
+ end
+
private
def cache_key_name
@@ -23,7 +29,14 @@ module Groups
end
def relation_for_count
- IssuesFinder.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: public_only?).execute
+ IssuesFinder.new(
+ user,
+ group_id: group.id,
+ state: 'opened',
+ non_archived: true,
+ include_subgroups: true,
+ public_only: public_only?
+ ).execute
end
def issuable_key
diff --git a/app/services/groups/participants_service.rb b/app/services/groups/participants_service.rb
new file mode 100644
index 00000000000..0844c98dd6a
--- /dev/null
+++ b/app/services/groups/participants_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Groups
+ class ParticipantsService < Groups::BaseService
+ include Users::ParticipableService
+
+ def execute(noteable)
+ @noteable = noteable
+
+ participants =
+ noteable_owner +
+ participants_in_noteable +
+ all_members +
+ groups +
+ group_members
+
+ render_participants_as_hash(participants.uniq)
+ end
+
+ def all_members
+ count = group_members.count
+ [{ username: "all", name: "All Group Members", count: count }]
+ end
+
+ def group_members
+ return [] unless noteable
+
+ @group_members ||= sorted(noteable.group.direct_and_indirect_users)
+ end
+ end
+end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index e800e546a45..56ff1310def 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -200,16 +200,16 @@ module Groups
end
def update_integrations
- @group.services.inherit.delete_all
- Service.create_from_active_default_integrations(@group, :group_id)
+ @group.integrations.inherit.delete_all
+ Integration.create_from_active_default_integrations(@group, :group_id)
end
def propagate_integrations
- @group.services.inherit.each do |integration|
+ @group.integrations.inherit.each do |integration|
PropagateIntegrationWorker.perform_async(integration.id)
end
end
end
end
-Groups::TransferService.prepend_if_ee('EE::Groups::TransferService')
+Groups::TransferService.prepend_mod_with('Groups::TransferService')
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index ff369d01efc..1ad43b051be 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -147,4 +147,4 @@ module Groups
end
end
-Groups::UpdateService.prepend_if_ee('EE::Groups::UpdateService')
+Groups::UpdateService.prepend_mod_with('Groups::UpdateService')
diff --git a/app/services/ide/schemas_config_service.rb b/app/services/ide/schemas_config_service.rb
index 8d2ce97103d..a013a4679b5 100644
--- a/app/services/ide/schemas_config_service.rb
+++ b/app/services/ide/schemas_config_service.rb
@@ -10,7 +10,7 @@ module Ide
def execute
schema = predefined_schema_for(params[:filename]) || {}
success(schema: schema)
- rescue => e
+ rescue StandardError => e
error(e.message)
end
@@ -46,4 +46,4 @@ module Ide
end
end
-Ide::SchemasConfigService.prepend_if_ee('::EE::Ide::SchemasConfigService')
+Ide::SchemasConfigService.prepend_mod_with('Ide::SchemasConfigService')
diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb
index 2683c75e41f..4a43b2f7425 100644
--- a/app/services/import/base_service.rb
+++ b/app/services/import/base_service.rb
@@ -18,7 +18,7 @@ module Import
group = Groups::NestedCreateService.new(current_user, group_path: namespace).execute
group.errors.any? ? current_user.namespace : group
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error(e)
current_user.namespace
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 3ee5a185f42..2f808d45ffd 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -122,4 +122,4 @@ module Import
end
end
-Import::GithubService.prepend_if_ee('EE::Import::GithubService')
+Import::GithubService.prepend_mod_with('Import::GithubService')
diff --git a/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb b/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb
new file mode 100644
index 00000000000..bbfdaf692f9
--- /dev/null
+++ b/app/services/import/gitlab_projects/create_project_from_remote_file_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Import
+ module GitlabProjects
+ class CreateProjectFromRemoteFileService < CreateProjectFromUploadedFileService
+ FILE_SIZE_LIMIT = 10.gigabytes
+ ALLOWED_CONTENT_TYPES = ['application/gzip'].freeze
+
+ validate :valid_remote_import_url?
+ validate :validate_file_size
+ validate :validate_content_type
+
+ private
+
+ def required_params
+ [:path, :namespace, :remote_import_url]
+ end
+
+ def project_params
+ super
+ .except(:file)
+ .merge(import_export_upload: ::ImportExportUpload.new(
+ remote_import_url: params[:remote_import_url]
+ ))
+ end
+
+ def valid_remote_import_url?
+ ::Gitlab::UrlBlocker.validate!(
+ params[:remote_import_url],
+ allow_localhost: allow_local_requests?,
+ allow_local_network: allow_local_requests?,
+ schemes: %w(http https)
+ )
+
+ true
+ rescue ::Gitlab::UrlBlocker::BlockedUrlError => e
+ errors.add(:base, e.message)
+
+ false
+ end
+
+ def allow_local_requests?
+ ::Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
+ end
+
+ def validate_content_type
+ if headers['content-type'].blank?
+ errors.add(:base, "Missing 'ContentType' header")
+ elsif !ALLOWED_CONTENT_TYPES.include?(headers['content-type'])
+ errors.add(:base, "Remote file content type '%{content_type}' not allowed. (Allowed content types: %{allowed})" % {
+ content_type: headers['content-type'],
+ allowed: ALLOWED_CONTENT_TYPES.join(',')
+ })
+ end
+ end
+
+ def validate_file_size
+ if headers['content-length'].to_i == 0
+ errors.add(:base, "Missing 'ContentLength' header")
+ elsif headers['content-length'].to_i > FILE_SIZE_LIMIT
+ errors.add(:base, 'Remote file larger than limit. (limit %{limit})' % {
+ limit: ActiveSupport::NumberHelper.number_to_human_size(FILE_SIZE_LIMIT)
+ })
+ end
+ end
+
+ def headers
+ return {} if params[:remote_import_url].blank? || !valid_remote_import_url?
+
+ @headers ||= Gitlab::HTTP.head(params[:remote_import_url]).headers
+ end
+ end
+ end
+end
diff --git a/app/services/import/gitlab_projects/create_project_from_uploaded_file_service.rb b/app/services/import/gitlab_projects/create_project_from_uploaded_file_service.rb
new file mode 100644
index 00000000000..35d52a11288
--- /dev/null
+++ b/app/services/import/gitlab_projects/create_project_from_uploaded_file_service.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Import
+ module GitlabProjects
+ class CreateProjectFromUploadedFileService
+ include ActiveModel::Validations
+ include ::Services::ReturnServiceResponses
+
+ validate :required_params_presence
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def execute
+ return error(errors.full_messages.first) unless valid?
+ return error(project.errors.full_messages&.first) unless project.saved?
+
+ success(project)
+ rescue StandardError => e
+ error(e.message)
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def error(message)
+ super(message, :bad_request)
+ end
+
+ def project
+ @project ||= ::Projects::GitlabProjectsImportService.new(
+ current_user,
+ project_params,
+ params[:override]
+ ).execute
+ end
+
+ def project_params
+ {
+ name: params[:name],
+ path: params[:path],
+ namespace_id: params[:namespace].id,
+ file: params[:file],
+ overwrite: params[:overwrite],
+ import_type: 'gitlab_project'
+ }
+ end
+
+ def required_params
+ [:path, :namespace, :file]
+ end
+
+ def required_params_presence
+ required_params
+ .select { |key| params[key].blank? }
+ .each do |missing_parameter|
+ errors.add(:base, "Parameter '#{missing_parameter}' is required")
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb
index cff288d602b..7497ee00d74 100644
--- a/app/services/incident_management/incidents/create_service.rb
+++ b/app/services/incident_management/incidents/create_service.rb
@@ -15,11 +15,13 @@ module IncidentManagement
def execute
issue = Issues::CreateService.new(
- project,
- current_user,
- title: title,
- description: description,
- issue_type: ISSUE_TYPE
+ project: project,
+ current_user: current_user,
+ params: {
+ title: title,
+ description: description,
+ issue_type: ISSUE_TYPE
+ }
).execute
return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
diff --git a/app/services/integrations/test/project_service.rb b/app/services/integrations/test/project_service.rb
index d72ca928c34..31c8f02c7b6 100644
--- a/app/services/integrations/test/project_service.rb
+++ b/app/services/integrations/test/project_service.rb
@@ -42,4 +42,4 @@ module Integrations
end
end
-Integrations::Test::ProjectService.prepend_if_ee('::EE::Integrations::Test::ProjectService')
+Integrations::Test::ProjectService.prepend_mod_with('Integrations::Test::ProjectService')
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
index 8bcbb92cd0e..cd32cd78728 100644
--- a/app/services/issuable/bulk_update_service.rb
+++ b/app/services/issuable/bulk_update_service.rb
@@ -15,9 +15,13 @@ module Issuable
def execute(type)
ids = params.delete(:issuable_ids).split(",")
set_update_params(type)
- items = update_issuables(type, ids)
+ updated_issuables = update_issuables(type, ids)
- response_success(payload: { count: items.size })
+ if updated_issuables.present? && requires_count_cache_reset?(type)
+ schedule_group_issues_count_reset(updated_issuables)
+ end
+
+ response_success(payload: { count: updated_issuables.size })
rescue ArgumentError => e
response_error(e.message, 422)
end
@@ -53,7 +57,7 @@ module Issuable
items.each do |issuable|
next unless can?(current_user, :"update_#{type}", issuable)
- update_class.new(issuable.issuing_parent, current_user, params).execute(issuable)
+ update_class.new(**update_class.constructor_container_arg(issuable.issuing_parent), current_user: current_user, params: params).execute(issuable)
end
items
@@ -81,7 +85,18 @@ module Issuable
def response_error(message, http_status)
ServiceResponse.error(message: message, http_status: http_status)
end
+
+ def requires_count_cache_reset?(type)
+ type.to_sym == :issue && params.include?(:state_event)
+ end
+
+ def schedule_group_issues_count_reset(updated_issuables)
+ group_ids = updated_issuables.map(&:project).map(&:namespace_id)
+ return if group_ids.empty?
+
+ Issuables::ClearGroupsIssueCounterWorker.perform_async(group_ids)
+ end
end
end
-Issuable::BulkUpdateService.prepend_if_ee('EE::Issuable::BulkUpdateService')
+Issuable::BulkUpdateService.prepend_mod_with('Issuable::BulkUpdateService')
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index 3861d88bce9..e1b4613726d 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -73,12 +73,17 @@ module Issuable
copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
event.attributes
- .except('id')
+ .except(*blocked_state_event_attributes)
.merge(entity_key => new_entity.id,
'state' => ResourceStateEvent.states[event.state])
end
end
+ # Overriden on EE::Issuable::Clone::AttributesRewriter
+ def blocked_state_event_attributes
+ ['id']
+ end
+
def event_attributes_with_milestone(event, milestone)
event.attributes
.except('id')
@@ -118,4 +123,4 @@ module Issuable
end
end
-Issuable::Clone::AttributesRewriter.prepend_if_ee('EE::Issuable::Clone::AttributesRewriter')
+Issuable::Clone::AttributesRewriter.prepend_mod_with('Issuable::Clone::AttributesRewriter')
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 3c2bc527b12..f8a9eb3ece5 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -65,7 +65,7 @@ module Issuable
end
def close_issue
- close_service = Issues::CloseService.new(old_project, current_user)
+ close_service = Issues::CloseService.new(project: old_project, current_user: current_user)
close_service.execute(original_entity, notifications: false, system_note: false)
end
@@ -88,4 +88,4 @@ module Issuable
end
end
-Issuable::Clone::BaseService.prepend_if_ee('EE::Issuable::Clone::BaseService')
+Issuable::Clone::BaseService.prepend_mod_with('Issuable::Clone::BaseService')
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index fd2dc3787c2..aedd0c377c6 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Issuable
- class CommonSystemNotesService < ::BaseService
+ class CommonSystemNotesService < ::BaseProjectService
attr_reader :issuable
def execute(issuable, old_labels: [], old_milestone: nil, is_update: true)
@@ -109,4 +109,4 @@ module Issuable
end
end
-Issuable::CommonSystemNotesService.prepend_if_ee('EE::Issuable::CommonSystemNotesService')
+Issuable::CommonSystemNotesService.prepend_mod_with('Issuable::CommonSystemNotesService')
diff --git a/app/services/issuable/destroy_label_links_service.rb b/app/services/issuable/destroy_label_links_service.rb
new file mode 100644
index 00000000000..6fff9b5e8d2
--- /dev/null
+++ b/app/services/issuable/destroy_label_links_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Issuable
+ class DestroyLabelLinksService
+ BATCH_SIZE = 100
+
+ def initialize(target_id, target_type)
+ @target_id = target_id
+ @target_type = target_type
+ end
+
+ def execute
+ inner_query =
+ LabelLink
+ .select(:id)
+ .for_target(target_id, target_type)
+ .limit(BATCH_SIZE)
+
+ delete_query = <<~SQL
+ DELETE FROM "#{LabelLink.table_name}"
+ WHERE id IN (#{inner_query.to_sql})
+ SQL
+
+ loop do
+ result = ActiveRecord::Base.connection.execute(delete_query)
+
+ break if result.cmd_tuples == 0
+ end
+ end
+
+ private
+
+ attr_reader :target_id, :target_type
+ end
+end
diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb
index d5aa84d8d6c..b75905fb5b0 100644
--- a/app/services/issuable/destroy_service.rb
+++ b/app/services/issuable/destroy_service.rb
@@ -3,15 +3,13 @@
module Issuable
class DestroyService < IssuableBaseService
def execute(issuable)
- if issuable.destroy
- after_destroy(issuable)
- end
+ after_destroy(issuable) if issuable.destroy
end
private
def after_destroy(issuable)
- delete_todos(issuable)
+ delete_associated_records(issuable)
issuable.update_project_counter_caches
issuable.assignees.each(&:invalidate_cache_counts)
end
@@ -20,19 +18,23 @@ module Issuable
issuable.resource_parent.group
end
- def delete_todos(issuable)
+ def delete_associated_records(issuable)
actor = group_for(issuable)
- if Feature.enabled?(:destroy_issuable_todos_async, actor, default_enabled: :yaml)
- TodosDestroyer::DestroyedIssuableWorker
- .perform_async(issuable.id, issuable.class.name)
- else
- TodosDestroyer::DestroyedIssuableWorker
- .new
- .perform(issuable.id, issuable.class.name)
- end
+ delete_todos(actor, issuable)
+ delete_label_links(actor, issuable)
+ end
+
+ def delete_todos(actor, issuable)
+ TodosDestroyer::DestroyedIssuableWorker
+ .perform_async(issuable.id, issuable.class.name)
+ end
+
+ def delete_label_links(actor, issuable)
+ Issuable::LabelLinksDestroyWorker
+ .perform_async(issuable.id, issuable.class.name)
end
end
end
-Issuable::DestroyService.prepend_if_ee('EE::Issuable::DestroyService')
+Issuable::DestroyService.prepend_mod_with('Issuable::DestroyService')
diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb
index 5a2665285de..27dbc8b3cc4 100644
--- a/app/services/issuable/import_csv/base_service.rb
+++ b/app/services/issuable/import_csv/base_service.rb
@@ -68,7 +68,7 @@ module Issuable
end
def create_issuable(attributes)
- create_issuable_class.new(@project, @user, attributes).execute
+ create_issuable_class.new(project: @project, current_user: @user, params: attributes).execute
end
def email_results_to_user
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index add53bc6267..099e0d81bc9 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -1,11 +1,21 @@
# frozen_string_literal: true
-class IssuableBaseService < BaseService
+class IssuableBaseService < ::BaseProjectService
private
+ def self.constructor_container_arg(value)
+ # TODO: Dynamically determining the type of a constructor arg based on the class is an antipattern,
+ # but the root cause is that Epics::BaseService has some issues that inheritance may not be the
+ # appropriate pattern. See more details in comments at the top of Epics::BaseService#initialize.
+ # Follow on issue to address this:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328438
+
+ { project: value }
+ end
+
attr_accessor :params, :skip_milestone_email
- def initialize(project, user = nil, params = {})
+ def initialize(project:, current_user: nil, params: {})
super
@skip_milestone_email = @params.delete(:skip_milestone_email)
@@ -343,9 +353,13 @@ class IssuableBaseService < BaseService
def change_state(issuable)
case params.delete(:state_event)
when 'reopen'
- reopen_service.new(project, current_user, {}).execute(issuable)
+ service_class = reopen_service
when 'close'
- close_service.new(project, current_user, {}).execute(issuable)
+ service_class = close_service
+ end
+
+ if service_class
+ service_class.new(**service_class.constructor_container_arg(project), current_user: current_user).execute(issuable)
end
end
@@ -406,7 +420,7 @@ class IssuableBaseService < BaseService
end
def create_system_notes(issuable, **options)
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, **options)
+ Issuable::CommonSystemNotesService.new(project: project, current_user: current_user).execute(issuable, **options)
end
def associations_before_update(issuable)
@@ -493,4 +507,4 @@ class IssuableBaseService < BaseService
end
end
-IssuableBaseService.prepend_if_ee('EE::IssuableBaseService')
+IssuableBaseService.prepend_mod_with('IssuableBaseService')
diff --git a/app/services/issuable_links/create_service.rb b/app/services/issuable_links/create_service.rb
index cbb81f1f521..81685f81afa 100644
--- a/app/services/issuable_links/create_service.rb
+++ b/app/services/issuable_links/create_service.rb
@@ -118,4 +118,4 @@ module IssuableLinks
end
end
-IssuableLinks::CreateService.prepend_if_ee('EE::IssuableLinks::CreateService')
+IssuableLinks::CreateService.prepend_mod_with('IssuableLinks::CreateService')
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
index 63762b1af79..a022d3e0bcf 100644
--- a/app/services/issue_links/create_service.rb
+++ b/app/services/issue_links/create_service.rb
@@ -43,4 +43,4 @@ module IssueLinks
end
end
-IssueLinks::CreateService.prepend_if_ee('EE::IssueLinks::CreateService')
+IssueLinks::CreateService.prepend_mod_with('IssueLinks::CreateService')
diff --git a/app/services/issue_rebalancing_service.rb b/app/services/issue_rebalancing_service.rb
index f9c3388204f..6a8d45b92b2 100644
--- a/app/services/issue_rebalancing_service.rb
+++ b/app/services/issue_rebalancing_service.rb
@@ -3,8 +3,18 @@
class IssueRebalancingService
MAX_ISSUE_COUNT = 10_000
BATCH_SIZE = 100
+ SMALLEST_BATCH_SIZE = 5
+ RETRIES_LIMIT = 3
TooManyIssues = Class.new(StandardError)
+ TIMING_CONFIGURATION = [
+ [0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms
+ [0.5.seconds, 0.05.seconds],
+ [1.second, 0.5.seconds],
+ [1.second, 0.5.seconds],
+ [5.seconds, 1.second]
+ ].freeze
+
def initialize(issue)
@issue = issue
@base = Issue.relative_positioning_query_base(issue)
@@ -23,14 +33,23 @@ class IssueRebalancingService
assign_positions(start, indexed_ids)
.sort_by(&:first)
.each_slice(BATCH_SIZE) do |pairs_with_position|
- update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
+ if Feature.enabled?(:issue_rebalancing_with_retry)
+ update_positions_with_retry(pairs_with_position, 'rebalance issue positions in batches ordered by id')
+ else
+ update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
+ end
end
end
else
Issue.transaction do
indexed_ids.each_slice(BATCH_SIZE) do |pairs|
pairs_with_position = assign_positions(start, pairs)
- update_positions(pairs_with_position, 'rebalance issue positions')
+
+ if Feature.enabled?(:issue_rebalancing_with_retry)
+ update_positions_with_retry(pairs_with_position, 'rebalance issue positions')
+ else
+ update_positions(pairs_with_position, 'rebalance issue positions')
+ end
end
end
end
@@ -52,12 +71,37 @@ class IssueRebalancingService
end
end
+ def update_positions_with_retry(pairs_with_position, query_name)
+ retries = 0
+ batch_size = pairs_with_position.size
+
+ until pairs_with_position.empty?
+ begin
+ update_positions(pairs_with_position.first(batch_size), query_name)
+ pairs_with_position = pairs_with_position.drop(batch_size)
+ retries = 0
+ rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => ex
+ raise ex if batch_size < SMALLEST_BATCH_SIZE
+
+ if (retries += 1) == RETRIES_LIMIT
+ # shrink the batch size in half when RETRIES limit is reached and update still fails perhaps because batch size is still too big
+ batch_size = (batch_size / 2).to_i
+ retries = 0
+ end
+
+ retry
+ end
+ end
+ end
+
def update_positions(pairs_with_position, query_name)
values = pairs_with_position.map do |id, index|
"(#{id}, #{index})"
end.join(', ')
- run_update_query(values, query_name)
+ Gitlab::Database::WithLockRetries.new(timing_configuration: TIMING_CONFIGURATION, klass: self.class).run do
+ run_update_query(values, query_name)
+ end
end
def run_update_query(values, query_name)
diff --git a/app/services/issues/after_create_service.rb b/app/services/issues/after_create_service.rb
index 0c6ec65f0e2..5d10eca2979 100644
--- a/app/services/issues/after_create_service.rb
+++ b/app/services/issues/after_create_service.rb
@@ -10,4 +10,4 @@ module Issues
end
end
-Issues::AfterCreateService.prepend_ee_mod
+Issues::AfterCreateService.prepend_mod
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 07e4a10708e..72e906e20f1 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -37,6 +37,8 @@ module Issues
def filter_params(issue)
super
+ params.delete(:issue_type) unless issue_type_allowed?(issue)
+
moved_issue = params.delete(:moved_issue)
# Setting created_at, updated_at and iid is allowed only for admins and owners or
@@ -75,7 +77,12 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
+
+ # @param object [Issue, Project]
+ def issue_type_allowed?(object)
+ can?(current_user, :"create_#{params[:issue_type]}", object)
+ end
end
end
-Issues::BaseService.prepend_if_ee('EE::Issues::BaseService')
+Issues::BaseService.prepend_mod_with('Issues::BaseService')
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 3145739fe91..5cb138946d7 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -64,20 +64,17 @@ module Issues
private
- def allowed_issue_base_params
- [:title, :description, :confidential, :issue_type]
- end
+ def allowed_issue_params
+ allowed_params = [
+ :title,
+ :description,
+ :confidential
+ ]
- def allowed_issue_admin_params
- [:milestone_id]
- end
+ allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
+ allowed_params << :issue_type if issue_type_allowed?(project)
- def allowed_issue_params
- if can?(current_user, :admin_issue, project)
- params.slice(*(allowed_issue_base_params + allowed_issue_admin_params))
- else
- params.slice(*allowed_issue_base_params)
- end
+ params.slice(*allowed_params)
end
def build_issue_params
@@ -88,4 +85,4 @@ module Issues
end
end
-Issues::BuildService.prepend_if_ee('EE::Issues::BuildService')
+Issues::BuildService.prepend_mod_with('Issues::BuildService')
diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb
index b64e4687a87..6df32f1104c 100644
--- a/app/services/issues/clone_service.rb
+++ b/app/services/issues/clone_service.rb
@@ -57,7 +57,7 @@ module Issues
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
- CreateService.new(target_project, current_user, new_params).execute(skip_system_notes: true)
+ CreateService.new(project: target_project, current_user: current_user, params: new_params).execute(skip_system_notes: true)
end
def queue_copy_designs
@@ -90,4 +90,4 @@ module Issues
end
end
-Issues::CloneService.prepend_if_ee('EE::Issues::CloneService')
+Issues::CloneService.prepend_mod_with('Issues::CloneService')
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 746f7d1f4c1..1700d1d8586 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -24,8 +24,8 @@ module Issues
return issue
end
- if project.issues_enabled? && issue.close
- issue.update(closed_by: current_user)
+ if project.issues_enabled? && issue.close(current_user)
+ remove_on_close_labels_from(issue)
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
@@ -52,6 +52,18 @@ module Issues
private
+ def remove_on_close_labels_from(issue)
+ old_labels = issue.labels.to_a
+
+ issue.label_links.with_remove_on_close_labels.delete_all
+ issue.labels.reset
+
+ Issuable::CommonSystemNotesService.new(project: project, current_user: current_user).execute(
+ issue,
+ old_labels: old_labels
+ )
+ end
+
def close_external_issue(issue, closed_via)
return unless project.external_issue_tracker&.support_close_issue?
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 68660b35bee..1f4efeb1a8a 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -8,7 +8,7 @@ module Issues
@request = params.delete(:request)
@spam_params = Spam::SpamActionService.filter_spam_params!(params, @request)
- @issue = BuildService.new(project, current_user, params).execute
+ @issue = BuildService.new(project: project, current_user: current_user, params: params).execute
filter_resolve_discussion_params
@@ -75,4 +75,4 @@ module Issues
end
end
-Issues::CreateService.prepend_ee_mod
+Issues::CreateService.prepend_mod
diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb
index feb496542c8..d150f0e5917 100644
--- a/app/services/issues/duplicate_service.rb
+++ b/app/services/issues/duplicate_service.rb
@@ -10,7 +10,7 @@ module Issues
create_issue_duplicate_note(duplicate_issue, canonical_issue)
create_issue_canonical_note(canonical_issue, duplicate_issue)
- close_service.new(project, current_user, {}).execute(duplicate_issue)
+ close_service.new(project: project, current_user: current_user).execute(duplicate_issue)
duplicate_issue.update(duplicated_to: canonical_issue)
relate_two_issues(duplicate_issue, canonical_issue)
diff --git a/app/services/issues/export_csv_service.rb b/app/services/issues/export_csv_service.rb
index dd43c77adfa..3809d8bc347 100644
--- a/app/services/issues/export_csv_service.rb
+++ b/app/services/issues/export_csv_service.rb
@@ -58,4 +58,4 @@ module Issues
end
end
-Issues::ExportCsvService.prepend_if_ee('EE::Issues::ExportCsvService')
+Issues::ExportCsvService.prepend_mod_with('Issues::ExportCsvService')
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index c1afb8f456d..e49123a2993 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -61,7 +61,7 @@ module Issues
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
- CreateService.new(@target_project, @current_user, new_params).execute(skip_system_notes: true)
+ CreateService.new(project: @target_project, current_user: @current_user, params: new_params).execute(skip_system_notes: true)
end
def queue_copy_designs
@@ -106,4 +106,4 @@ module Issues
end
end
-Issues::MoveService.prepend_if_ee('EE::Issues::MoveService')
+Issues::MoveService.prepend_mod_with('Issues::MoveService')
diff --git a/app/services/issues/related_branches_service.rb b/app/services/issues/related_branches_service.rb
index 98d8412102f..8b08c1f8ddb 100644
--- a/app/services/issues/related_branches_service.rb
+++ b/app/services/issues/related_branches_service.rb
@@ -30,7 +30,7 @@ module Issues
def branches_with_merge_request_for(issue)
Issues::ReferencedMergeRequestsService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.referenced_merge_requests(issue)
.map(&:source_branch)
end
diff --git a/app/services/issues/reorder_service.rb b/app/services/issues/reorder_service.rb
index c82ad6ea501..9c5fbec7d8e 100644
--- a/app/services/issues/reorder_service.rb
+++ b/app/services/issues/reorder_service.rb
@@ -21,7 +21,7 @@ module Issues
end
def update(issue, attrs)
- ::Issues::UpdateService.new(project, current_user, attrs).execute(issue)
+ ::Issues::UpdateService.new(project: project, current_user: current_user, params: attrs).execute(issue)
rescue ActiveRecord::RecordNotFound
false
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 702527d80a7..af5029f8364 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -24,7 +24,7 @@ module Issues
def filter_params(issue)
super
- # filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filtr_params`
+ # filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filter_params`
# because we do allow users that cannot admin issues to set confidential flag when creating an issue
unless can_admin_issuable?(issue)
params.delete(:confidential)
@@ -42,10 +42,6 @@ module Issues
).execute(spam_params: spam_params)
end
- def after_update(issue)
- IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
- end
-
def handle_changes(issue, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
@@ -61,12 +57,7 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.assignees != old_assignees
- create_assignee_note(issue, old_assignees)
- notification_service.async.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_assignable(issue, current_user, old_assignees)
- track_incident_action(current_user, issue, :incident_assigned)
- end
+ handle_assignee_changes(issue, old_assignees)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
@@ -90,12 +81,27 @@ module Issues
end
end
+ def handle_assignee_changes(issue, old_assignees)
+ return if issue.assignees == old_assignees
+
+ create_assignee_note(issue, old_assignees)
+ notification_service.async.reassigned_issue(issue, current_user, old_assignees)
+ todo_service.reassigned_assignable(issue, current_user, old_assignees)
+ track_incident_action(current_user, issue, :incident_assigned)
+
+ if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
+ GraphqlTriggers.issuable_assignees_updated(issue)
+ end
+ end
+
def handle_task_changes(issuable)
todo_service.resolve_todos_for_target(issuable, current_user)
todo_service.update_issue(issuable, current_user)
end
def handle_move_between_ids(issue)
+ issue.check_repositioning_allowed! if params[:move_between_ids]
+
super
rebalance_if_needed(issue)
@@ -113,7 +119,7 @@ module Issues
canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
if canonical_issue
- Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue)
+ Issues::DuplicateService.new(project: project, current_user: current_user).execute(issue, canonical_issue)
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -126,7 +132,7 @@ module Issues
target_project != issue.project
update(issue)
- Issues::MoveService.new(project, current_user).execute(issue, target_project)
+ Issues::MoveService.new(project: project, current_user: current_user).execute(issue, target_project)
end
private
@@ -142,14 +148,14 @@ module Issues
# we've pre-empted this from running in #execute, so let's go ahead and update the Issue now.
update(issue)
- Issues::CloneService.new(project, current_user).execute(issue, target_project, with_notes: with_notes)
+ Issues::CloneService.new(project: project, current_user: current_user).execute(issue, target_project, with_notes: with_notes)
end
def create_merge_request_from_quick_action
create_merge_request_params = params.delete(:create_merge_request)
return unless create_merge_request_params
- MergeRequests::CreateFromIssueService.new(project, current_user, create_merge_request_params).execute
+ MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_merge_request_params).execute
end
def handle_milestone_change(issue)
@@ -201,4 +207,4 @@ module Issues
end
end
-Issues::UpdateService.prepend_if_ee('EE::Issues::UpdateService')
+Issues::UpdateService.prepend_mod_with('Issues::UpdateService')
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index 1384e2f83b2..ef48134dec4 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -2,10 +2,10 @@
module Issues
class ZoomLinkService < Issues::BaseService
- def initialize(issue, user)
- super(issue.project, user)
+ def initialize(project:, current_user:, params:)
+ super
- @issue = issue
+ @issue = params.fetch(:issue)
@added_meeting = ZoomMeeting.canonical_meeting(@issue)
end
diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb
index 88cfe684125..c9ffdeb2a16 100644
--- a/app/services/jira_import/start_import_service.rb
+++ b/app/services/jira_import/start_import_service.rb
@@ -41,7 +41,7 @@ module JiraImport
project.save! && jira_import.schedule!
ServiceResponse.success(payload: { import_data: jira_import } )
- rescue => ex
+ rescue StandardError => ex
# in case project.save! raises an error
Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
jira_import&.do_fail!(error_message: ex.message)
diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb
index c1c3ef8792f..507537391ed 100644
--- a/app/services/keys/create_service.rb
+++ b/app/services/keys/create_service.rb
@@ -19,4 +19,4 @@ module Keys
end
end
-Keys::CreateService.prepend_if_ee('EE::Keys::CreateService')
+Keys::CreateService.prepend_mod_with('Keys::CreateService')
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
index 4552c5cf9a2..eaf5eb35f58 100644
--- a/app/services/keys/destroy_service.rb
+++ b/app/services/keys/destroy_service.rb
@@ -13,4 +13,4 @@ module Keys
end
end
-Keys::DestroyService.prepend_if_ee('EE::Keys::DestroyService')
+Keys::DestroyService.prepend_mod_with('Keys::DestroyService')
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index 1d022740c44..ff29358df86 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -14,15 +14,16 @@ module Labels
return [] unless labels
- labels = labels.split(',') if labels.is_a?(String)
+ labels = labels.split(',').map(&:strip) if labels.is_a?(String)
+ existing_labels = LabelsFinder.new(current_user, finder_params(labels)).execute.index_by(&:title)
labels.map do |label_name|
label = Labels::FindOrCreateService.new(
current_user,
parent,
include_ancestor_groups: true,
- title: label_name.strip,
- available_labels: available_labels
+ title: label_name,
+ existing_labels_by_title: existing_labels
).execute(find_only: find_only)
label
@@ -45,18 +46,19 @@ module Labels
private
- def finder_params
- params = { include_ancestor_groups: true }
+ def finder_params(titles = nil)
+ finder_params = { include_ancestor_groups: true }
+ finder_params[:title] = titles if titles
case parent
when Group
- params[:group_id] = parent.id
- params[:only_group_labels] = true
+ finder_params[:group_id] = parent.id
+ finder_params[:only_group_labels] = true
when Project
- params[:project_id] = parent.id
+ finder_params[:project_id] = parent.id
end
- params
+ finder_params
end
end
end
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
index 665d1035b2b..6c070d15cdb 100644
--- a/app/services/labels/create_service.rb
+++ b/app/services/labels/create_service.rb
@@ -26,4 +26,4 @@ module Labels
end
end
-Labels::CreateService.prepend_if_ee('EE::Labels::CreateService')
+Labels::CreateService.prepend_mod_with('Labels::CreateService')
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index a47dd42aea0..112d156c214 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -6,6 +6,7 @@ module Labels
@current_user = current_user
@parent = parent
@available_labels = params.delete(:available_labels)
+ @existing_labels_by_title = params.delete(:existing_labels_by_title)
@params = params.dup.with_indifferent_access
end
@@ -16,7 +17,7 @@ module Labels
private
- attr_reader :current_user, :parent, :params, :skip_authorization
+ attr_reader :current_user, :parent, :params, :skip_authorization, :existing_labels_by_title
def available_labels
@available_labels ||= LabelsFinder.new(
@@ -29,9 +30,8 @@ module Labels
# Only creates the label if current_user can do so, if the label does not exist
# and the user can not create the label, nil is returned
- # rubocop: disable CodeReuse/ActiveRecord
def find_or_create_label(find_only: false)
- new_label = available_labels.find_by(title: title)
+ new_label = find_existing_label(title)
return new_label if find_only
@@ -42,6 +42,13 @@ module Labels
new_label
end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_existing_label(title)
+ return existing_labels_by_title[title] if existing_labels_by_title
+
+ available_labels.find_by(title: title)
+ end
# rubocop: enable CodeReuse/ActiveRecord
def title
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index fdf2cf13f92..e3b110f8f26 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -101,4 +101,4 @@ module Labels
end
end
-Labels::PromoteService.prepend_if_ee('EE::Labels::PromoteService')
+Labels::PromoteService.prepend_mod_with('Labels::PromoteService')
diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb
index 1b283018c16..54f193c86e6 100644
--- a/app/services/lfs/lock_file_service.rb
+++ b/app/services/lfs/lock_file_service.rb
@@ -12,7 +12,7 @@ module Lfs
error('already locked', 409, current_lock)
rescue Gitlab::GitAccess::ForbiddenError => ex
error(ex.message, 403)
- rescue => ex
+ rescue StandardError => ex
error(ex.message, 500)
end
@@ -42,4 +42,4 @@ module Lfs
end
end
-Lfs::LockFileService.prepend_if_ee('EE::Lfs::LockFileService')
+Lfs::LockFileService.prepend_mod_with('Lfs::LockFileService')
diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb
index 192ce3d3c2a..a77be643478 100644
--- a/app/services/lfs/locks_finder_service.rb
+++ b/app/services/lfs/locks_finder_service.rb
@@ -4,7 +4,7 @@ module Lfs
class LocksFinderService < BaseService
def execute
success(locks: find_locks)
- rescue => ex
+ rescue StandardError => ex
error(ex.message, 500)
end
diff --git a/app/services/lfs/push_service.rb b/app/services/lfs/push_service.rb
index 9b947fbed07..e21988aa561 100644
--- a/app/services/lfs/push_service.rb
+++ b/app/services/lfs/push_service.rb
@@ -16,12 +16,17 @@ module Lfs
end
success
- rescue => err
+ rescue StandardError => err
+ Gitlab::ErrorTracking.log_exception(err, extra_context)
error(err.message)
end
private
+ def extra_context
+ { project_id: project.id, user_id: current_user&.id }.compact
+ end
+
# Currently we only set repository_type for design repository objects, so
# push mirroring must send objects with a `nil` repository type - but if the
# wiki repository uses LFS, its objects will also be sent. This will be
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index a13e89904a0..7a3025ee7ea 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -12,7 +12,7 @@ module Lfs
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
error(_('Lock not found'), 404)
- rescue => ex
+ rescue StandardError => ex
error(ex.message, 500)
end
@@ -46,4 +46,4 @@ module Lfs
end
end
-Lfs::UnlockFileService.prepend_if_ee('EE::Lfs::UnlockFileService')
+Lfs::UnlockFileService.prepend_mod_with('Lfs::UnlockFileService')
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index e79c5f69a30..919c22894c1 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -21,4 +21,4 @@ module Members
end
end
-Members::ApproveAccessRequestService.prepend_if_ee('EE::Members::ApproveAccessRequestService')
+Members::ApproveAccessRequestService.prepend_mod_with('Members::ApproveAccessRequestService')
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 953cf7f5bf6..7b81cc27635 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -98,4 +98,4 @@ module Members
end
end
-Members::CreateService.prepend_if_ee('EE::Members::CreateService')
+Members::CreateService.prepend_mod_with('Members::CreateService')
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 8cad065e6cc..bb2d419c046 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -117,4 +117,4 @@ module Members
end
end
-Members::DestroyService.prepend_if_ee('EE::Members::DestroyService')
+Members::DestroyService.prepend_mod_with('Members::DestroyService')
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 5c6e51201c2..257698f65ae 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -31,4 +31,4 @@ module Members
end
end
-Members::UpdateService.prepend_if_ee('EE::Members::UpdateService')
+Members::UpdateService.prepend_mod_with('Members::UpdateService')
diff --git a/app/services/merge_request_metrics_service.rb b/app/services/merge_request_metrics_service.rb
index 9ea71838011..d86bcca8892 100644
--- a/app/services/merge_request_metrics_service.rb
+++ b/app/services/merge_request_metrics_service.rb
@@ -20,4 +20,4 @@ class MergeRequestMetricsService
end
end
-MergeRequestMetricsService.prepend_if_ee('EE::MergeRequestMetricsService')
+MergeRequestMetricsService.prepend_mod_with('MergeRequestMetricsService')
diff --git a/app/services/merge_requests/add_context_service.rb b/app/services/merge_requests/add_context_service.rb
index 77b00f645c9..7b441ddf5e4 100644
--- a/app/services/merge_requests/add_context_service.rb
+++ b/app/services/merge_requests/add_context_service.rb
@@ -50,7 +50,7 @@ module MergeRequests
def duplicates
existing_oids = merge_request.merge_request_context_commits.map { |commit| commit.sha.to_s }
existing_oids.select do |existing_oid|
- commit_ids.select { |commit_id| existing_oid.start_with?(commit_id) }.count > 0
+ commit_ids.count { |commit_id| existing_oid.start_with?(commit_id) } > 0
end
end
diff --git a/app/services/merge_requests/add_spent_time_service.rb b/app/services/merge_requests/add_spent_time_service.rb
new file mode 100644
index 00000000000..ae79645a96a
--- /dev/null
+++ b/app/services/merge_requests/add_spent_time_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class AddSpentTimeService < UpdateService
+ def execute(merge_request)
+ old_associations = { total_time_spent: merge_request.total_time_spent }
+
+ merge_request.spend_time(params[:spend_time])
+
+ merge_request_saved = merge_request.with_transaction_returning_status do
+ merge_request.save
+ end
+
+ if merge_request_saved
+ create_system_notes(merge_request)
+
+ # track usage
+ track_time_spend_edits(merge_request, old_associations[:total_time_spent])
+
+ execute_hooks(merge_request, 'update', old_associations: old_associations)
+ end
+
+ merge_request
+ end
+
+ private
+
+ def track_time_spend_edits(merge_request, old_total_time_spent)
+ if old_total_time_spent != merge_request.total_time_spent
+ merge_request_activity_counter.track_time_spent_changed_action(user: current_user)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index ed9747a8c99..77564521d45 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -35,9 +35,9 @@ module MergeRequests
end
def link_lfs_objects(merge_request)
- LinkLfsObjectsService.new(merge_request.target_project).execute(merge_request)
+ LinkLfsObjectsService.new(project: merge_request.target_project).execute(merge_request)
end
end
end
-MergeRequests::AfterCreateService.prepend_if_ee('EE::MergeRequests::AfterCreateService')
+MergeRequests::AfterCreateService.prepend_mod_with('MergeRequests::AfterCreateService')
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 59d8f553eff..62e599e3e27 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -54,4 +54,4 @@ module MergeRequests
end
end
-MergeRequests::ApprovalService.prepend_if_ee('EE::MergeRequests::ApprovalService')
+MergeRequests::ApprovalService.prepend_mod_with('MergeRequests::ApprovalService')
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
index e9107b9998e..f016c16e816 100644
--- a/app/services/merge_requests/assign_issues_service.rb
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module MergeRequests
- class AssignIssuesService < BaseService
+ class AssignIssuesService < BaseProjectService
def assignable_issues
@assignable_issues ||= begin
if current_user == merge_request.author
@@ -16,7 +16,7 @@ module MergeRequests
def execute
assignable_issues.each do |issue|
- Issues::UpdateService.new(issue.project, current_user, assignee_ids: [current_user.id]).execute(issue)
+ Issues::UpdateService.new(project: issue.project, current_user: current_user, params: { assignee_ids: [current_user.id] }).execute(issue)
end
{
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 3a3765355d8..e94274aff9d 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -147,7 +147,7 @@ module MergeRequests
if async
MergeRequests::CreatePipelineWorker.perform_async(project.id, user.id, merge_request.id)
else
- MergeRequests::CreatePipelineService.new(project, user).execute(merge_request)
+ MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
end
end
@@ -208,4 +208,4 @@ module MergeRequests
end
end
-MergeRequests::BaseService.prepend_if_ee('EE::MergeRequests::BaseService')
+MergeRequests::BaseService.prepend_mod_with('MergeRequests::BaseService')
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index ecc55eae5de..878e42172b7 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -300,4 +300,4 @@ module MergeRequests
end
end
-MergeRequests::BuildService.prepend_if_ee('EE::MergeRequests::BuildService')
+MergeRequests::BuildService.prepend_mod_with('MergeRequests::BuildService')
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index b43e697d3ab..12fc828b194 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -2,16 +2,28 @@
module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService
- def initialize(project, user, params)
+ # TODO: This constructor does not use the "params:" argument from the superclass,
+ # but instead has a custom "mr_params:" argument. This is because historically,
+ # prior to named arguments being introduced to the constructor, it never passed
+ # along the third positional argument when calling `super`.
+ # This should be changed, in order to be consistent (all subclasses should pass
+ # along all of the arguments to the superclass, otherwise it is probably not an
+ # "is a" relationship). However, we need to be sure that passing the params
+ # argument to `super` (especially target_project_id) will not cause any unexpected
+ # behavior in the superclass. Since the addition of the named arguments is
+ # intended to be a low-risk pure refactor, we will defer this fix
+ # to this follow-on issue:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328726
+ def initialize(project:, current_user:, mr_params: {})
# branch - the name of new branch
# ref - the source of new branch.
- @branch_name = params[:branch_name]
- @issue_iid = params[:issue_iid]
- @ref = params[:ref]
- @target_project_id = params[:target_project_id]
+ @branch_name = mr_params[:branch_name]
+ @issue_iid = mr_params[:issue_iid]
+ @ref = mr_params[:ref]
+ @target_project_id = mr_params[:target_project_id]
- super(project, user)
+ super(project: project, current_user: current_user)
end
def execute
@@ -73,11 +85,11 @@ module MergeRequests
end
def default_branch
- target_project.default_branch || 'master'
+ target_project.default_branch_or_main
end
def merge_request
- MergeRequests::BuildService.new(target_project, current_user, merge_request_params).execute
+ MergeRequests::BuildService.new(project: target_project, current_user: current_user, params: merge_request_params).execute
end
def merge_request_params
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index 46c4c102091..ebeba0ee5b8 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -63,4 +63,4 @@ module MergeRequests
end
end
-MergeRequests::CreatePipelineService.prepend_if_ee('EE::MergeRequests::CreatePipelineService')
+MergeRequests::CreatePipelineService.prepend_mod_with('MergeRequests::CreatePipelineService')
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 8186472ec65..c1292d924b2 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -53,4 +53,4 @@ module MergeRequests
end
end
-MergeRequests::CreateService.include_if_ee('EE::MergeRequests::CreateService')
+MergeRequests::CreateService.include_mod_with('MergeRequests::CreateService')
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index de3f2acdf63..7996fcb5273 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -1,13 +1,7 @@
# frozen_string_literal: true
module MergeRequests
- class GetUrlsService < BaseService
- attr_reader :project
-
- def initialize(project)
- @project = project
- end
-
+ class GetUrlsService < BaseProjectService
def execute(changes)
return [] unless project&.printing_merge_request_link_enabled
diff --git a/app/services/merge_requests/handle_assignees_change_service.rb b/app/services/merge_requests/handle_assignees_change_service.rb
index 77ff0791eb4..9ac386110f7 100644
--- a/app/services/merge_requests/handle_assignees_change_service.rb
+++ b/app/services/merge_requests/handle_assignees_change_service.rb
@@ -3,17 +3,13 @@
module MergeRequests
class HandleAssigneesChangeService < MergeRequests::BaseService
def async_execute(merge_request, old_assignees, options = {})
- if Feature.enabled?(:async_handle_merge_request_assignees_change, merge_request.target_project, default_enabled: :yaml)
- MergeRequests::HandleAssigneesChangeWorker
- .perform_async(
- merge_request.id,
- current_user.id,
- old_assignees.map(&:id),
- options
- )
- else
- execute(merge_request, old_assignees, options)
- end
+ MergeRequests::HandleAssigneesChangeWorker
+ .perform_async(
+ merge_request.id,
+ current_user.id,
+ old_assignees.map(&:id),
+ options
+ )
end
def execute(merge_request, old_assignees, options = {})
@@ -40,4 +36,4 @@ module MergeRequests
end
end
-MergeRequests::HandleAssigneesChangeService.prepend_if_ee('EE::MergeRequests::HandleAssigneesChangeService')
+MergeRequests::HandleAssigneesChangeService.prepend_mod_with('MergeRequests::HandleAssigneesChangeService')
diff --git a/app/services/merge_requests/link_lfs_objects_service.rb b/app/services/merge_requests/link_lfs_objects_service.rb
index 191da594095..4981d3efcae 100644
--- a/app/services/merge_requests/link_lfs_objects_service.rb
+++ b/app/services/merge_requests/link_lfs_objects_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module MergeRequests
- class LinkLfsObjectsService < ::BaseService
+ class LinkLfsObjectsService < ::BaseProjectService
def execute(merge_request, oldrev: merge_request.diff_base_sha, newrev: merge_request.diff_head_sha)
return if merge_request.source_project == project
return if no_changes?(oldrev, newrev)
diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
index fe09c92aab9..3b9d3bccacf 100644
--- a/app/services/merge_requests/merge_base_service.rb
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -61,7 +61,7 @@ module MergeRequests
def squash_sha!
params[:merge_request] = merge_request
- squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute
+ squash_result = ::MergeRequests::SquashService.new(project: project, current_user: current_user, params: params).execute
case squash_result[:status]
when :success
@@ -73,4 +73,4 @@ module MergeRequests
end
end
-MergeRequests::MergeBaseService.prepend_if_ee('EE::MergeRequests::MergeBaseService')
+MergeRequests::MergeBaseService.prepend_mod_with('MergeRequests::MergeBaseService')
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 27f474b0fe7..5e7eee4f1c3 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -8,16 +8,22 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::MergeBaseService
+ include Gitlab::Utils::StrongMemoize
+
GENERIC_ERROR_MESSAGE = 'An error occurred while merging'
+ LEASE_TIMEOUT = 15.minutes.to_i
delegate :merge_jid, :state, to: :@merge_request
def execute(merge_request, options = {})
if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService)
- FfMergeService.new(project, current_user, params).execute(merge_request)
+ FfMergeService.new(project: project, current_user: current_user, params: params).execute(merge_request)
return
end
+ return if merge_request.merged?
+ return unless exclusive_lease(merge_request.id).try_obtain
+
@merge_request = merge_request
@options = options
@@ -34,6 +40,8 @@ module MergeRequests
log_info("Merge process finished on JID #{merge_jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
+ ensure
+ exclusive_lease(merge_request.id).cancel
end
private
@@ -96,14 +104,14 @@ module MergeRequests
rescue Gitlab::Git::PreReceiveError => e
raise MergeError,
"Something went wrong during merge pre-receive hook. #{e.message}".strip
- rescue => e
+ rescue StandardError => e
handle_merge_error(log_message: e.message)
raise_error(GENERIC_ERROR_MESSAGE)
end
def after_merge
log_info("Post merge started on JID #{merge_jid} with state #{state}")
- MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+ MergeRequests::PostMergeService.new(project: project, current_user: current_user).execute(merge_request)
log_info("Post merge finished on JID #{merge_jid} with state #{state}")
if delete_source_branch?
@@ -146,5 +154,13 @@ module MergeRequests
# loaded from the database they're strings
params.with_indifferent_access[:sha] == merge_request.diff_head_sha
end
+
+ def exclusive_lease(merge_request_id)
+ strong_memoize(:"exclusive_lease_#{merge_request_id}") do
+ lease_key = ['merge_requests_merge_service', merge_request_id].join(':')
+
+ Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+ end
+ end
end
end
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
index 9fecab85cc1..3e294aeaa07 100644
--- a/app/services/merge_requests/mergeability_check_service.rb
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -157,7 +157,7 @@ module MergeRequests
def merge_to_ref
params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) }
- result = MergeRequests::MergeToRefService.new(project, merge_request.author, params).execute(merge_request)
+ result = MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author, params: params).execute(merge_request)
result[:status] == :success
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 4d7d632ee14..ea3071b3c2d 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -12,20 +12,28 @@ module MergeRequests
MAX_RETARGET_MERGE_REQUESTS = 4
def execute(merge_request)
+ return if merge_request.merged?
+
+ # Mark the merge request as merged, everything that happens afterwards is
+ # executed once
merge_request.mark_as_merged
- close_issues(merge_request)
- todo_service.merge_merge_request(merge_request, current_user)
+
create_event(merge_request)
- create_note(merge_request)
+ todo_service.merge_merge_request(merge_request, current_user)
+
merge_request_activity_counter.track_merge_mr_action(user: current_user)
+
+ create_note(merge_request)
+ close_issues(merge_request)
notification_service.merge_mr(merge_request, current_user)
- execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
cancel_review_app_jobs!(merge_request)
cleanup_environments(merge_request)
cleanup_refs(merge_request)
+
+ execute_hooks(merge_request, 'merge')
end
private
@@ -36,7 +44,7 @@ module MergeRequests
closed_issues = merge_request.visible_closing_issues_for(current_user)
closed_issues.each do |issue|
- Issues::CloseService.new(project, current_user).execute(issue, commit: merge_request)
+ Issues::CloseService.new(project: project, current_user: current_user).execute(issue, commit: merge_request)
end
end
@@ -59,4 +67,4 @@ module MergeRequests
end
end
-MergeRequests::PostMergeService.prepend_if_ee('EE::MergeRequests::PostMergeService')
+MergeRequests::PostMergeService.prepend_mod_with('MergeRequests::PostMergeService')
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index 05ec87c7d60..cc1e08e1606 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
module MergeRequests
- class PushOptionsHandlerService
+ class PushOptionsHandlerService < ::BaseProjectService
LIMIT = 10
- attr_reader :current_user, :errors, :changes,
- :project, :push_options, :target_project
+ attr_reader :errors, :changes,
+ :push_options, :target_project
+
+ def initialize(project:, current_user:, params: {}, changes:, push_options:)
+ super(project: project, current_user: current_user, params: params)
- def initialize(project, current_user, changes, push_options)
- @project = project
@target_project = @project.default_merge_request_target
- @current_user = current_user
@changes = Gitlab::ChangesList.new(changes)
@push_options = push_options
@errors = []
@@ -95,16 +95,16 @@ module MergeRequests
# Use BuildService to assign the standard attributes of a merge request
merge_request = ::MergeRequests::BuildService.new(
- project,
- current_user,
- create_params(branch)
+ project: project,
+ current_user: current_user,
+ params: create_params(branch)
).execute
unless merge_request.errors.present?
merge_request = ::MergeRequests::CreateService.new(
- project,
- current_user,
- merge_request.attributes.merge(assignees: merge_request.assignees,
+ project: project,
+ current_user: current_user,
+ params: merge_request.attributes.merge(assignees: merge_request.assignees,
label_ids: merge_request.label_ids)
).execute
end
@@ -114,9 +114,9 @@ module MergeRequests
def update!(merge_request)
merge_request = ::MergeRequests::UpdateService.new(
- target_project,
- current_user,
- update_params(merge_request)
+ project: target_project,
+ current_user: current_user,
+ params: update_params(merge_request)
).execute(merge_request)
collect_errors_from_merge_request(merge_request) unless merge_request.valid?
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 87808a21a15..ae8398e2335 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -27,7 +27,7 @@ module MergeRequests
repository.rebase(current_user, merge_request, skip_ci: @skip_ci)
true
- rescue => e
+ rescue StandardError => e
log_error(exception: e, message: REBASE_ERROR, save_message_on_model: true)
false
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index e04c5168cef..d5e2595a9c6 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -62,7 +62,7 @@ module MergeRequests
# the latest diff state as the last _valid_ one.
merge_requests_for_source_branch.reject(&:source_branch_exists?).each do |mr|
MergeRequests::CloseService
- .new(mr.target_project, @current_user)
+ .new(project: mr.target_project, current_user: @current_user)
.execute(mr)
end
end
@@ -96,7 +96,7 @@ module MergeRequests
merge_request.merge_commit_sha = analyzer.get_merge_commit(merge_request.diff_head_sha)
MergeRequests::PostMergeService
- .new(merge_request.target_project, @current_user)
+ .new(project: merge_request.target_project, current_user: @current_user)
.execute(merge_request)
end
end
@@ -109,7 +109,7 @@ module MergeRequests
merge_requests_for_forks.find_each do |mr|
LinkLfsObjectsService
- .new(mr.target_project)
+ .new(project: mr.target_project)
.execute(mr, oldrev: @push.oldrev, newrev: @push.newrev)
end
end
@@ -162,12 +162,7 @@ module MergeRequests
end
def refresh_pipelines_on_merge_requests(merge_request)
- if Feature.enabled?(:code_review_async_pipeline_creation, project, default_enabled: :yaml)
- create_pipeline_for(merge_request, current_user, async: true)
- else
- create_pipeline_for(merge_request, current_user, async: false)
- UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
- end
+ create_pipeline_for(merge_request, current_user, async: true)
end
def abort_auto_merges(merge_request)
@@ -218,7 +213,7 @@ module MergeRequests
# If the a commit no longer exists in this repo, gitlab_git throws
# a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52
@commits = @project.repository.commits_between(common_ref, @push.newrev) if common_ref
- rescue
+ rescue StandardError
end
elsif @push.branch_removed?
# No commits for a deleted branch.
@@ -309,4 +304,4 @@ module MergeRequests
end
end
-MergeRequests::RefreshService.prepend_if_ee('EE::MergeRequests::RefreshService')
+MergeRequests::RefreshService.prepend_mod_with('MergeRequests::RefreshService')
diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb
index f2bf5de61c1..872e7e0c89c 100644
--- a/app/services/merge_requests/remove_approval_service.rb
+++ b/app/services/merge_requests/remove_approval_service.rb
@@ -41,4 +41,4 @@ module MergeRequests
end
end
-MergeRequests::RemoveApprovalService.prepend_if_ee('EE::MergeRequests::RemoveApprovalService')
+MergeRequests::RemoveApprovalService.prepend_mod_with('MergeRequests::RemoveApprovalService')
diff --git a/app/services/merge_requests/resolve_todos_service.rb b/app/services/merge_requests/resolve_todos_service.rb
index 0010b596eee..2d322a7de30 100644
--- a/app/services/merge_requests/resolve_todos_service.rb
+++ b/app/services/merge_requests/resolve_todos_service.rb
@@ -10,11 +10,7 @@ module MergeRequests
end
def async_execute
- if Feature.enabled?(:resolve_merge_request_todos_async, merge_request.target_project, default_enabled: :yaml)
- MergeRequests::ResolveTodosWorker.perform_async(merge_request.id, user.id)
- else
- execute
- end
+ MergeRequests::ResolveTodosWorker.perform_async(merge_request.id, user.id)
end
def execute
diff --git a/app/services/merge_requests/retarget_chain_service.rb b/app/services/merge_requests/retarget_chain_service.rb
index e8101e447d2..dab6e198979 100644
--- a/app/services/merge_requests/retarget_chain_service.rb
+++ b/app/services/merge_requests/retarget_chain_service.rb
@@ -24,9 +24,11 @@ module MergeRequests
next unless can?(current_user, :update_merge_request, other_merge_request.source_project)
::MergeRequests::UpdateService
- .new(other_merge_request.source_project, current_user,
- target_branch: merge_request.target_branch,
- target_branch_was_deleted: true)
+ .new(project: other_merge_request.source_project, current_user: current_user,
+ params: {
+ target_branch: merge_request.target_branch,
+ target_branch_was_deleted: true
+ })
.execute(other_merge_request)
end
end
diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb
index faa2e921581..31c49b3ae70 100644
--- a/app/services/merge_requests/squash_service.rb
+++ b/app/services/merge_requests/squash_service.rb
@@ -29,7 +29,7 @@ module MergeRequests
squash_sha = repository.squash(current_user, merge_request, message || merge_request.default_squash_commit_message)
success(squash_sha: squash_sha)
- rescue => e
+ rescue StandardError => e
log_error(exception: e, message: 'Failed to squash merge request')
false
@@ -37,7 +37,7 @@ module MergeRequests
def squash_in_progress?
merge_request.squash_in_progress?
- rescue => e
+ rescue StandardError => e
log_error(exception: e, message: 'Failed to check squash in progress')
raise SquashInProgressError, e.message
diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb
index b339a644e8c..f99db35fd49 100644
--- a/app/services/merge_requests/update_assignees_service.rb
+++ b/app/services/merge_requests/update_assignees_service.rb
@@ -20,7 +20,7 @@ module MergeRequests
# Defer the more expensive operations (handle_assignee_changes) to the background
MergeRequests::HandleAssigneesChangeService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.async_execute(merge_request, old_assignees, execute_hooks: true)
merge_request
@@ -45,7 +45,7 @@ module MergeRequests
end
def assignee_ids
- params.fetch(:assignee_ids).first(1)
+ params.fetch(:assignee_ids).reject { _1 == 0 }.first(1)
end
def params
@@ -61,4 +61,4 @@ module MergeRequests
end
end
-MergeRequests::UpdateAssigneesService.prepend_if_ee('EE::MergeRequests::UpdateAssigneesService')
+MergeRequests::UpdateAssigneesService.prepend_mod_with('MergeRequests::UpdateAssigneesService')
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 8995c5f2411..b613d88aee4 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
class UpdateService < MergeRequests::BaseService
extend ::Gitlab::Utils::Override
- def initialize(project, user = nil, params = {})
+ def initialize(project:, current_user: nil, params: {})
super
@target_branch_was_deleted = @params.delete(:target_branch_was_deleted)
@@ -222,7 +222,7 @@ module MergeRequests
def handle_assignees_change(merge_request, old_assignees)
MergeRequests::HandleAssigneesChangeService
- .new(project, current_user)
+ .new(project: project, current_user: current_user)
.async_execute(merge_request, old_assignees)
end
@@ -295,6 +295,8 @@ module MergeRequests
case attribute
when :assignee_ids
assignees_service.execute(merge_request)
+ when :spend_time
+ add_time_spent_service.execute(merge_request)
else
nil
end
@@ -302,9 +304,13 @@ module MergeRequests
def assignees_service
@assignees_service ||= ::MergeRequests::UpdateAssigneesService
- .new(project, current_user, params)
+ .new(project: project, current_user: current_user, params: params)
+ end
+
+ def add_time_spent_service
+ @add_time_spent_service ||= ::MergeRequests::AddSpentTimeService.new(project: project, current_user: current_user, params: params)
end
end
end
-MergeRequests::UpdateService.prepend_if_ee('EE::MergeRequests::UpdateService')
+MergeRequests::UpdateService.prepend_mod_with('MergeRequests::UpdateService')
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index 6069d236e82..e94c8d92c3a 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -80,7 +80,7 @@ module Metrics
def fetch_dashboard
uid = GrafanaUidParser.new(grafana_url, project).parse
- raise DashboardProcessingError.new(_('Dashboard uid not found')) unless uid
+ raise DashboardProcessingError, _('Dashboard uid not found') unless uid
response = client.get_dashboard(uid: uid)
@@ -89,7 +89,7 @@ module Metrics
def fetch_datasource(dashboard)
name = DatasourceNameParser.new(grafana_url, dashboard).parse
- raise DashboardProcessingError.new(_('Datasource name not found')) unless name
+ raise DashboardProcessingError, _('Datasource name not found') unless name
response = client.get_datasource(name: name)
@@ -115,7 +115,7 @@ module Metrics
def parse_json(json)
Gitlab::Json.parse(json, symbolize_names: true)
rescue JSON::ParserError
- raise DashboardProcessingError.new(_('Grafana response contains invalid json'))
+ raise DashboardProcessingError, _('Grafana response contains invalid json')
end
end
diff --git a/app/services/metrics/dashboard/transient_embed_service.rb b/app/services/metrics/dashboard/transient_embed_service.rb
index 0a9c4bc7b86..29ea9909a36 100644
--- a/app/services/metrics/dashboard/transient_embed_service.rb
+++ b/app/services/metrics/dashboard/transient_embed_service.rb
@@ -39,7 +39,7 @@ module Metrics
end
def invalid_embed_json!(message)
- raise DashboardProcessingError.new(_("Parsing error for param :embed_json. %{message}") % { message: message })
+ raise DashboardProcessingError, _("Parsing error for param :embed_json. %{message}") % { message: message }
end
end
end
diff --git a/app/services/metrics/dashboard/update_dashboard_service.rb b/app/services/metrics/dashboard/update_dashboard_service.rb
index d990e96ecb5..0574cb15e96 100644
--- a/app/services/metrics/dashboard/update_dashboard_service.rb
+++ b/app/services/metrics/dashboard/update_dashboard_service.rb
@@ -58,7 +58,7 @@ module Metrics
target_branch: project.default_branch,
title: params[:commit_message]
}
- merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
+ merge_request = ::MergeRequests::CreateService.new(project: project, current_user: current_user, params: merge_request_params).execute
if merge_request.persisted?
success(result.merge(merge_request: Gitlab::UrlBuilder.build(merge_request)))
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 87c7a282081..2563f2f5390 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -7,11 +7,11 @@ module Milestones
update_params = { milestone: nil, skip_milestone_email: true }
milestone.issues.each do |issue|
- Issues::UpdateService.new(parent, current_user, update_params).execute(issue)
+ Issues::UpdateService.new(project: parent, current_user: current_user, params: update_params).execute(issue)
end
milestone.merge_requests.each do |merge_request|
- MergeRequests::UpdateService.new(parent, current_user, update_params).execute(merge_request)
+ MergeRequests::UpdateService.new(project: parent, current_user: current_user, params: update_params).execute(merge_request)
end
log_destroy_event_for(milestone)
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index 2431318cbb2..4417f17f33e 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -90,4 +90,4 @@ module Milestones
end
end
-Milestones::PromoteService.prepend_if_ee('EE::Milestones::PromoteService')
+Milestones::PromoteService.prepend_mod_with('Milestones::PromoteService')
diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb
index 782c6bc3e35..b9a12a35d31 100644
--- a/app/services/milestones/update_service.rb
+++ b/app/services/milestones/update_service.rb
@@ -21,4 +21,4 @@ module Milestones
end
end
-Milestones::UpdateService.prepend_if_ee('EE::Milestones::UpdateService')
+Milestones::UpdateService.prepend_mod_with('Milestones::UpdateService')
diff --git a/app/services/namespace_settings/update_service.rb b/app/services/namespace_settings/update_service.rb
index c6c04b63690..de54eb87cc0 100644
--- a/app/services/namespace_settings/update_service.rb
+++ b/app/services/namespace_settings/update_service.rb
@@ -35,4 +35,4 @@ module NamespaceSettings
end
end
-NamespaceSettings::UpdateService.prepend_if_ee('EE::NamespaceSettings::UpdateService')
+NamespaceSettings::UpdateService.prepend_mod_with('NamespaceSettings::UpdateService')
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index eb81253bc08..61d5ed3bdf4 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -66,7 +66,6 @@ module Namespaces
Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group)
end
- # rubocop: disable CodeReuse/ActiveRecord
def groups_for_track
onboarding_progress_scope = OnboardingProgress
.completed_actions_with_latest_in_range(completed_actions, range)
@@ -75,9 +74,18 @@ module Namespaces
# Filtering out sub-groups is a temporary fix to prevent calling
# `.root_ancestor` on groups that are not root groups.
# See https://gitlab.com/groups/gitlab-org/-/epics/5594 for more information.
- Group.where(parent_id: nil).joins(:onboarding_progress).merge(onboarding_progress_scope)
+ Group
+ .top_most
+ .with_onboarding_progress
+ .merge(onboarding_progress_scope)
+ .merge(subscription_scope)
+ end
+
+ def subscription_scope
+ {}
end
+ # rubocop: disable CodeReuse/ActiveRecord
def users_for_group(group)
group.users
.where(email_opted_in: true)
@@ -136,3 +144,5 @@ module Namespaces
end
end
end
+
+Namespaces::InProductMarketingEmailsService.prepend_mod
diff --git a/app/services/namespaces/package_settings/update_service.rb b/app/services/namespaces/package_settings/update_service.rb
index 0964963647a..cbadbe5c907 100644
--- a/app/services/namespaces/package_settings/update_service.rb
+++ b/app/services/namespaces/package_settings/update_service.rb
@@ -5,7 +5,10 @@ module Namespaces
class UpdateService < BaseContainerService
include Gitlab::Utils::StrongMemoize
- ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed maven_duplicate_exception_regex].freeze
+ ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed
+ maven_duplicate_exception_regex
+ generic_duplicates_allowed
+ generic_duplicate_exception_regex].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
diff --git a/app/services/namespaces/statistics_refresher_service.rb b/app/services/namespaces/statistics_refresher_service.rb
index c07b302839b..805060cdee9 100644
--- a/app/services/namespaces/statistics_refresher_service.rb
+++ b/app/services/namespaces/statistics_refresher_service.rb
@@ -9,7 +9,7 @@ module Namespaces
root_storage_statistics.recalculate!
rescue ActiveRecord::ActiveRecordError => e
- raise RefresherError.new(e.message)
+ raise RefresherError, e.message
end
private
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index e63099a0820..542fafb901b 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -133,4 +133,4 @@ module Notes
end
end
-Notes::CreateService.prepend_if_ee('EE::Notes::CreateService')
+Notes::CreateService.prepend_mod_with('Notes::CreateService')
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index 85f54a39add..c25b1ab0379 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -24,4 +24,4 @@ module Notes
end
end
-Notes::DestroyService.prepend_if_ee('EE::Notes::DestroyService')
+Notes::DestroyService.prepend_mod_with('Notes::DestroyService')
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 4f3b2000e9a..b7ccdbc1cff 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -44,4 +44,4 @@ module Notes
end
end
-Notes::PostProcessService.prepend_if_ee('EE::Notes::PostProcessService')
+Notes::PostProcessService.prepend_mod_with('Notes::PostProcessService')
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index 36d9f1d7867..900ace24ab4 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -24,12 +24,12 @@ module Notes
UPDATE_SERVICES
end
- def self.noteable_update_service(note)
+ def self.noteable_update_service_class(note)
update_services[note.noteable_type]
end
def self.supported?(note)
- !!noteable_update_service(note)
+ !!noteable_update_service_class(note)
end
def supported?(note)
@@ -55,9 +55,23 @@ module Notes
update_params[:spend_time][:note_id] = note.id
end
- self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable)
+ noteable_update_service_class = self.class.noteable_update_service_class(note)
+
+ # TODO: This conditional is necessary because we have not fully converted all possible
+ # noteable_update_service_class classes to use named arguments. See more details
+ # on the partial conversion at https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59182
+ # Follow-on issue to address this is here:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328734
+ service =
+ if noteable_update_service_class.respond_to?(:constructor_container_arg)
+ noteable_update_service_class.new(**noteable_update_service_class.constructor_container_arg(note.resource_parent), current_user: current_user, params: update_params)
+ else
+ noteable_update_service_class.new(note.resource_parent, current_user, update_params)
+ end
+
+ service.execute(note.noteable)
end
end
end
-Notes::QuickActionsService.prepend_if_ee('EE::Notes::QuickActionsService')
+Notes::QuickActionsService.prepend_mod_with('Notes::QuickActionsService')
diff --git a/app/services/notes/resolve_service.rb b/app/services/notes/resolve_service.rb
index cf24795f050..75ce9e27c5b 100644
--- a/app/services/notes/resolve_service.rb
+++ b/app/services/notes/resolve_service.rb
@@ -5,7 +5,7 @@ module Notes
def execute(note)
note.resolve!(current_user)
- ::MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+ ::MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(note.noteable)
end
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 76f9b6369b3..1cbb5916107 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -107,4 +107,4 @@ module Notes
end
end
-Notes::UpdateService.prepend_if_ee('EE::Notes::UpdateService')
+Notes::UpdateService.prepend_mod_with('Notes::UpdateService')
diff --git a/app/services/notification_recipients/builder/base.rb b/app/services/notification_recipients/builder/base.rb
index b41b969ad7c..e8f783136cc 100644
--- a/app/services/notification_recipients/builder/base.rb
+++ b/app/services/notification_recipients/builder/base.rb
@@ -100,7 +100,7 @@ module NotificationRecipients
# Get project/group users with CUSTOM notification level
# rubocop: disable CodeReuse/ActiveRecord
def add_custom_notifications
- return new_add_custom_notifications if Feature.enabled?(:notification_setting_recipient_refactor, project)
+ return new_add_custom_notifications if Feature.enabled?(:notification_setting_recipient_refactor, project, default_enabled: :yaml)
user_ids = []
@@ -172,6 +172,8 @@ module NotificationRecipients
# Get project users with WATCH notification level
# rubocop: disable CodeReuse/ActiveRecord
def project_watchers
+ return new_project_watchers if Feature.enabled?(:notification_setting_recipient_refactor, project, default_enabled: :yaml)
+
project_members_ids = user_ids_notifiable_on(project)
user_ids_with_project_global = user_ids_notifiable_on(project, :global)
@@ -184,16 +186,38 @@ module NotificationRecipients
user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
end
+
+ def new_project_watchers
+ notification_by_sources = related_notification_settings_sources(:watch)
+
+ return if notification_by_sources.blank?
+
+ user_ids = NotificationSetting.from_union(notification_by_sources).select(:user_id)
+
+ user_scope.where(id: user_ids)
+ end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def group_watchers
+ return new_group_watchers if Feature.enabled?(:notification_setting_recipient_refactor, project, default_enabled: :yaml)
+
user_ids_with_group_global = user_ids_notifiable_on(group, :global)
user_ids = user_ids_with_global_level_watch(user_ids_with_group_global)
user_ids_with_group_setting = select_group_members_ids(group, [], user_ids_with_group_global, user_ids)
user_scope.where(id: user_ids_with_group_setting)
end
+
+ def new_group_watchers
+ return [] unless group
+
+ user_ids = group
+ .notification_settings
+ .where(source_or_global_setting_by_level_query(:watch)).select(:user_id)
+
+ user_scope.where(id: user_ids)
+ end
# rubocop: enable CodeReuse/ActiveRecord
def add_subscribed_users
diff --git a/app/services/notification_recipients/builder/default.rb b/app/services/notification_recipients/builder/default.rb
index 19527ba84e6..58b0cd510c9 100644
--- a/app/services/notification_recipients/builder/default.rb
+++ b/app/services/notification_recipients/builder/default.rb
@@ -74,4 +74,4 @@ module NotificationRecipients
end
end
-NotificationRecipients::Builder::Default.prepend_if_ee('EE::NotificationRecipients::Builder::Default')
+NotificationRecipients::Builder::Default.prepend_mod_with('NotificationRecipients::Builder::Default')
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6f1f3309ad9..9dfcfe748da 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -384,6 +384,7 @@ class NotificationService
def send_service_desk_notification(note)
return unless note.noteable_type == 'Issue'
+ return if note.confidential
issue = note.noteable
recipients = issue.email_participants_emails
@@ -875,4 +876,4 @@ class NotificationService
end
end
-NotificationService.prepend_if_ee('EE::NotificationService')
+NotificationService.prepend_mod_with('NotificationService')
diff --git a/app/services/packages/debian/extract_changes_metadata_service.rb b/app/services/packages/debian/extract_changes_metadata_service.rb
index eb5baa7e53f..43a4db5bdfc 100644
--- a/app/services/packages/debian/extract_changes_metadata_service.rb
+++ b/app/services/packages/debian/extract_changes_metadata_service.rb
@@ -20,7 +20,7 @@ module Packages
files: files
}
rescue ActiveModel::ValidationError => e
- raise ExtractionError.new(e.message)
+ raise ExtractionError, e.message
end
private
@@ -41,10 +41,10 @@ module Packages
def files
strong_memoize(:files) do
- raise ExtractionError.new("is not a changes file") unless file_type == :changes
- raise ExtractionError.new("Files field is missing") if fields['Files'].blank?
- raise ExtractionError.new("Checksums-Sha1 field is missing") if fields['Checksums-Sha1'].blank?
- raise ExtractionError.new("Checksums-Sha256 field is missing") if fields['Checksums-Sha256'].blank?
+ raise ExtractionError, "is not a changes file" unless file_type == :changes
+ raise ExtractionError, "Files field is missing" if fields['Files'].blank?
+ raise ExtractionError, "Checksums-Sha1 field is missing" if fields['Checksums-Sha1'].blank?
+ raise ExtractionError, "Checksums-Sha256 field is missing" if fields['Checksums-Sha256'].blank?
init_entries_from_files
entries_from_checksums_sha1
@@ -73,8 +73,8 @@ module Packages
each_lines_for('Checksums-Sha1') do |line|
sha1sum, size, filename = line.split
entry = @entries[filename]
- raise ExtractionError.new("#{filename} is listed in Checksums-Sha1 but not in Files") unless entry
- raise ExtractionError.new("Size for #{filename} in Files and Checksums-Sha1 differ") unless entry.size == size.to_i
+ raise ExtractionError, "#{filename} is listed in Checksums-Sha1 but not in Files" unless entry
+ raise ExtractionError, "Size for #{filename} in Files and Checksums-Sha1 differ" unless entry.size == size.to_i
entry.sha1sum = sha1sum
end
@@ -84,8 +84,8 @@ module Packages
each_lines_for('Checksums-Sha256') do |line|
sha256sum, size, filename = line.split
entry = @entries[filename]
- raise ExtractionError.new("#{filename} is listed in Checksums-Sha256 but not in Files") unless entry
- raise ExtractionError.new("Size for #{filename} in Files and Checksums-Sha256 differ") unless entry.size == size.to_i
+ raise ExtractionError, "#{filename} is listed in Checksums-Sha256 but not in Files" unless entry
+ raise ExtractionError, "Size for #{filename} in Files and Checksums-Sha256 differ" unless entry.size == size.to_i
entry.sha256sum = sha256sum
end
@@ -104,7 +104,7 @@ module Packages
entry.package_file = ::Packages::PackageFileFinder.new(@package_file.package, filename).execute!
entry.validate!
rescue ActiveRecord::RecordNotFound
- raise ExtractionError.new("#{filename} is listed in Files but was not uploaded")
+ raise ExtractionError, "#{filename} is listed in Files but was not uploaded"
end
end
end
diff --git a/app/services/packages/debian/extract_metadata_service.rb b/app/services/packages/debian/extract_metadata_service.rb
index 015f472c7c9..f94587919b9 100644
--- a/app/services/packages/debian/extract_metadata_service.rb
+++ b/app/services/packages/debian/extract_metadata_service.rb
@@ -12,7 +12,7 @@ module Packages
end
def execute
- raise ExtractionError.new('invalid package file') unless valid_package_file?
+ raise ExtractionError, 'invalid package file' unless valid_package_file?
extract_metadata
end
diff --git a/app/services/packages/debian/generate_distribution_key_service.rb b/app/services/packages/debian/generate_distribution_key_service.rb
new file mode 100644
index 00000000000..28c97c7681e
--- /dev/null
+++ b/app/services/packages/debian/generate_distribution_key_service.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class GenerateDistributionKeyService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(current_user:, params: {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ raise ArgumentError, 'Please provide a user' unless current_user.is_a?(User)
+
+ generate_key
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def passphrase
+ strong_memoize(:passphrase) do
+ params[:passphrase] || ::User.random_password
+ end
+ end
+
+ def pinentry_script_content
+ escaped_passphrase = Shellwords.escape(passphrase)
+
+ <<~EOF
+ #!/bin/sh
+
+ echo OK Pleased to meet you
+
+ while read -r cmd; do
+ case "$cmd" in
+ GETPIN) echo D #{escaped_passphrase}; echo OK;;
+ *) echo OK;;
+ esac
+ done
+ EOF
+ end
+
+ def using_pinentry
+ Gitlab::Gpg.using_tmp_keychain do
+ home_dir = Gitlab::Gpg.current_home_dir
+
+ File.write("#{home_dir}/pinentry.sh", pinentry_script_content, mode: 'w', perm: 0755)
+
+ File.write("#{home_dir}/gpg-agent.conf", "pinentry-program #{home_dir}/pinentry.sh\n", mode: 'w')
+
+ GPGME::Ctx.new(armor: true, offline: true) do |ctx|
+ yield ctx
+ end
+ end
+ end
+
+ def generate_key_params
+ # https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html
+ '<GnupgKeyParms format="internal">' + "\n" +
+ {
+ 'Key-Type': params[:key_type] || 'RSA',
+ 'Key-Length': params[:key_length] || 4096,
+ 'Key-Usage': params[:key_usage] || 'sign',
+ 'Name-Real': params[:name_real] || 'GitLab Debian repository',
+ 'Name-Email': params[:name_email] || Gitlab.config.gitlab.email_reply_to,
+ 'Name-Comment': params[:name_comment] || 'GitLab Debian repository automatic signing key',
+ 'Expire-Date': params[:expire_date] || 0,
+ 'Passphrase': passphrase
+ }.map { |k, v| "#{k}: #{v}\n" }.join +
+ '</GnupgKeyParms>'
+ end
+
+ def generate_key
+ using_pinentry do |ctx|
+ # Generate key
+ ctx.generate_key generate_key_params
+
+ key = ctx.keys.first # rubocop:disable Gitlab/KeysFirstAndValuesFirst
+ fingerprint = key.fingerprint
+
+ # Export private key
+ data = GPGME::Data.new
+ ctx.export_keys fingerprint, data, GPGME::EXPORT_MODE_SECRET
+ data.seek 0
+ private_key = data.read
+
+ # Export public key
+ data = GPGME::Data.new
+ ctx.export_keys fingerprint, data
+ data.seek 0
+ public_key = data.read
+
+ {
+ private_key: private_key,
+ public_key: public_key,
+ passphrase: passphrase,
+ fingerprint: fingerprint
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/debian/generate_distribution_service.rb b/app/services/packages/debian/generate_distribution_service.rb
new file mode 100644
index 00000000000..67348af1a49
--- /dev/null
+++ b/app/services/packages/debian/generate_distribution_service.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class GenerateDistributionService
+ include Gitlab::Utils::StrongMemoize
+ include ExclusiveLeaseGuard
+
+ # used by ExclusiveLeaseGuard
+ DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
+
+ # From https://salsa.debian.org/ftp-team/dak/-/blob/991aaa27a7f7aa773bb9c0cf2d516e383d9cffa0/setup/core-init.d/080_metadatakeys#L9
+ BINARIES_METADATA = %w(
+ Package
+ Source
+ Binary
+ Version
+ Essential
+ Installed-Size
+ Maintainer
+ Uploaders
+ Original-Maintainer
+ Build-Depends
+ Build-Depends-Indep
+ Build-Conflicts
+ Build-Conflicts-Indep
+ Architecture
+ Standards-Version
+ Format
+ Files
+ Dm-Upload-Allowed
+ Vcs-Browse
+ Vcs-Hg
+ Vcs-Darcs
+ Vcs-Svn
+ Vcs-Git
+ Vcs-Browser
+ Vcs-Arch
+ Vcs-Bzr
+ Vcs-Mtn
+ Vcs-Cvs
+ Checksums-Sha256
+ Checksums-Sha1
+ Replaces
+ Provides
+ Depends
+ Pre-Depends
+ Recommends
+ Suggests
+ Enhances
+ Conflicts
+ Breaks
+ Description
+ Origin
+ Bugs
+ Multi-Arch
+ Homepage
+ Tag
+ Package-Type
+ Installer-Menu-Item
+ ).freeze
+
+ def initialize(distribution)
+ @distribution = distribution
+ @last_generated_at = nil
+ @md5sum = []
+ @sha256 = []
+ end
+
+ def execute
+ try_obtain_lease do
+ @distribution.transaction do
+ @last_generated_at = @distribution.component_files.maximum(:created_at)
+ generate_component_files
+ generate_release
+ destroy_old_component_files
+ end
+ end
+ end
+
+ private
+
+ def generate_component_files
+ @distribution.components.ordered_by_name.each do |component|
+ @distribution.architectures.ordered_by_name.each do |architecture|
+ generate_component_file(component, :packages, architecture, :deb)
+ end
+ end
+ end
+
+ def generate_component_file(component, component_file_type, architecture, package_file_type)
+ paragraphs = @distribution.package_files
+ .preload_debian_file_metadata
+ .with_debian_component_name(component.name)
+ .with_debian_architecture_name(architecture.name)
+ .with_debian_file_type(package_file_type)
+ .find_each
+ .map(&method(:package_stanza_from_fields))
+ create_component_file(component, component_file_type, architecture, package_file_type, paragraphs.join("\n"))
+ end
+
+ def package_stanza_from_fields(package_file)
+ [
+ BINARIES_METADATA.map do |metadata_key|
+ rfc822_field(metadata_key, package_file.debian_fields[metadata_key])
+ end,
+ rfc822_field('Section', package_file.debian_fields['Section'] || 'misc'),
+ rfc822_field('Priority', package_file.debian_fields['Priority'] || 'extra'),
+ rfc822_field('Filename', package_filename(package_file)),
+ rfc822_field('Size', package_file.size),
+ rfc822_field('MD5sum', package_file.file_md5),
+ rfc822_field('SHA256', package_file.file_sha256)
+ ].flatten.compact.join('')
+ end
+
+ def package_filename(package_file)
+ letter = package_file.package.name.start_with?('lib') ? package_file.package.name[0..3] : package_file.package.name[0]
+ "#{pool_prefix(package_file)}/#{letter}/#{package_file.package.name}/#{package_file.file_name}"
+ end
+
+ def pool_prefix(package_file)
+ case @distribution
+ when ::Packages::Debian::GroupDistribution
+ "pool/#{@distribution.codename}/#{package_file.package.project_id}"
+ else
+ "pool/#{@distribution.codename}/#{@distribution.container_id}"
+ end
+ end
+
+ def create_component_file(component, component_file_type, architecture, package_file_type, content)
+ component_file = component.files.create!(
+ file_type: component_file_type,
+ architecture: architecture,
+ compression_type: nil,
+ file: CarrierWaveStringFile.new(content),
+ file_md5: Digest::MD5.hexdigest(content),
+ file_sha256: Digest::SHA256.hexdigest(content)
+ )
+ @md5sum.append(" #{component_file.file_md5} #{component_file.size.to_s.rjust(8)} #{component_file.relative_path}")
+ @sha256.append(" #{component_file.file_sha256} #{component_file.size.to_s.rjust(8)} #{component_file.relative_path}")
+ end
+
+ def generate_release
+ @distribution.file = CarrierWaveStringFile.new(release_header + release_sums)
+ @distribution.updated_at = release_date
+ @distribution.save!
+ end
+
+ def release_header
+ strong_memoize(:release_header) do
+ [
+ %w[origin label suite version codename].map do |attribute|
+ rfc822_field(attribute.capitalize, @distribution.attributes[attribute])
+ end,
+ rfc822_field('Date', release_date.to_formatted_s(:rfc822)),
+ valid_until_field,
+ rfc822_field('NotAutomatic', !@distribution.automatic, !@distribution.automatic),
+ rfc822_field('ButAutomaticUpgrades', @distribution.automatic_upgrades, !@distribution.automatic && @distribution.automatic_upgrades),
+ rfc822_field('Architectures', @distribution.architectures.map { |architecture| architecture.name }.sort.join(' ')),
+ rfc822_field('Components', @distribution.components.map { |component| component.name }.sort.join(' ')),
+ rfc822_field('Description', @distribution.description)
+ ].flatten.compact.join('')
+ end
+ end
+
+ def release_date
+ strong_memoize(:release_date) do
+ Time.now.utc
+ end
+ end
+
+ def release_sums
+ ["MD5Sum:", @md5sum, "SHA256:", @sha256].flatten.compact.join("\n") + "\n"
+ end
+
+ def rfc822_field(name, value, condition = true)
+ return unless condition
+ return if value.blank?
+
+ "#{name}: #{value.to_s.gsub("\n\n", "\n.\n").gsub("\n", "\n ")}\n"
+ end
+
+ def valid_until_field
+ return unless @distribution.valid_time_duration_seconds
+
+ rfc822_field('Valid-Until', release_date.since(@distribution.valid_time_duration_seconds).to_formatted_s(:rfc822))
+ end
+
+ def destroy_old_component_files
+ # Only keep the last generation and one hour before
+ return if @last_generated_at.nil?
+
+ @distribution.component_files.created_before(@last_generated_at - 1.hour).destroy_all # rubocop:disable Cop/DestroyAll
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_key
+ "packages:debian:generate_distribution_service:distribution:#{@distribution.id}"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_timeout
+ DEFAULT_LEASE_TIMEOUT
+ end
+ end
+ end
+end
diff --git a/app/services/packages/generic/create_package_file_service.rb b/app/services/packages/generic/create_package_file_service.rb
index 1451a022a39..42a191fb415 100644
--- a/app/services/packages/generic/create_package_file_service.rb
+++ b/app/services/packages/generic/create_package_file_service.rb
@@ -23,6 +23,10 @@ module Packages
.new(project, current_user, package_params)
.execute
+ unless Namespace::PackageSetting.duplicates_allowed?(package)
+ raise ::Packages::DuplicatePackageError if target_file_is_duplicate?(package)
+ end
+
package.update_column(:status, params[:status]) if params[:status] && params[:status] != package.status
package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present?
@@ -40,6 +44,10 @@ module Packages
::Packages::CreatePackageFileService.new(package, file_params).execute
end
+
+ def target_file_is_duplicate?(package)
+ package.package_files.with_file_name(params[:file_name]).exists?
+ end
end
end
end
diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb
index a6cffa3038c..c7ffd468864 100644
--- a/app/services/packages/maven/find_or_create_package_service.rb
+++ b/app/services/packages/maven/find_or_create_package_service.rb
@@ -6,7 +6,7 @@ module Packages
def execute
package =
- ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project)
+ ::Packages::Maven::PackageFinder.new(current_user, project, path: params[:path])
.execute
unless Namespace::PackageSetting.duplicates_allowed?(package)
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 59125669f7d..dd5f1bcc936 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -26,7 +26,7 @@ module Packages
end
def execute
- raise ExtractionError.new('invalid package file') unless valid_package_file?
+ raise ExtractionError, 'invalid package file' unless valid_package_file?
extract_metadata(nuspec_file)
end
@@ -94,8 +94,8 @@ module Packages
Zip::File.open(file_path) do |zip_file|
entry = zip_file.glob('*.nuspec').first
- raise ExtractionError.new('nuspec file not found') unless entry
- raise ExtractionError.new('nuspec file too big') if entry.size > MAX_FILE_SIZE
+ raise ExtractionError, 'nuspec file not found' unless entry
+ raise ExtractionError, 'nuspec file too big' if entry.size > MAX_FILE_SIZE
entry.get_input_stream.read
end
diff --git a/app/services/packages/nuget/search_service.rb b/app/services/packages/nuget/search_service.rb
index 1eead1e62b3..fea424b3aa8 100644
--- a/app/services/packages/nuget/search_service.rb
+++ b/app/services/packages/nuget/search_service.rb
@@ -103,6 +103,7 @@ module Packages
def nuget_packages
Packages::Package.nuget
+ .displayable
.has_version
.without_nuget_temporary_name
end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 1bcab00bd92..8210072eab3 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -16,7 +16,7 @@ module Packages
end
def execute
- raise InvalidMetadataError.new('package name and/or package version not found in metadata') unless valid_metadata?
+ raise InvalidMetadataError, 'package name and/or package version not found in metadata' unless valid_metadata?
try_obtain_lease do
@package_file.transaction do
@@ -33,7 +33,7 @@ module Packages
end
end
rescue ActiveRecord::RecordInvalid => e
- raise InvalidMetadataError.new(e.message)
+ raise InvalidMetadataError, e.message
end
private
@@ -45,7 +45,7 @@ module Packages
::Packages::UpdateTagsService
.new(package, package_tags)
.execute
- rescue => e
+ rescue StandardError => e
raise InvalidMetadataError, e.message
end
diff --git a/app/services/packages/pypi/create_package_service.rb b/app/services/packages/pypi/create_package_service.rb
index cb8d9559dc9..b988c191734 100644
--- a/app/services/packages/pypi/create_package_service.rb
+++ b/app/services/packages/pypi/create_package_service.rb
@@ -13,7 +13,7 @@ module Packages
)
unless meta.valid?
- raise ActiveRecord::RecordInvalid.new(meta)
+ raise ActiveRecord::RecordInvalid, meta
end
Packages::Pypi::Metadatum.upsert(meta.attributes)
diff --git a/app/services/packages/rubygems/process_gem_service.rb b/app/services/packages/rubygems/process_gem_service.rb
index 59bf2a1ec28..109c87a0444 100644
--- a/app/services/packages/rubygems/process_gem_service.rb
+++ b/app/services/packages/rubygems/process_gem_service.rb
@@ -16,6 +16,7 @@ module Packages
end
def execute
+ raise ExtractionError, 'Gem was not processed - package_file is not set' unless package_file
return success if process_gem
error('Gem was not processed')
@@ -26,8 +27,6 @@ module Packages
attr_reader :package_file
def process_gem
- return false unless package_file
-
try_obtain_lease do
package.transaction do
rename_package_and_set_version
@@ -106,8 +105,8 @@ module Packages
Packages::PackageFile.find(package_file.id).file.use_file do |file_path|
Gem::Package.new(File.open(file_path))
end
- rescue
- raise ExtractionError.new('Unable to read gem file')
+ rescue StandardError
+ raise ExtractionError, 'Unable to read gem file'
end
# used by ExclusiveLeaseGuard
diff --git a/app/services/packages/terraform_module/create_package_service.rb b/app/services/packages/terraform_module/create_package_service.rb
new file mode 100644
index 00000000000..fc376c70b00
--- /dev/null
+++ b/app/services/packages/terraform_module/create_package_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Packages
+ module TerraformModule
+ class CreatePackageService < ::Packages::CreatePackageService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ return error('Version is empty.', 400) if params[:module_version].blank?
+ return error('Package already exists.', 403) if current_package_exists_elsewhere?
+ return error('Package version already exists.', 403) if current_package_version_exists?
+ return error('File is too large.', 400) if file_size_exceeded?
+
+ ActiveRecord::Base.transaction { create_terraform_module_package! }
+ end
+
+ private
+
+ def create_terraform_module_package!
+ package = create_package!(:terraform_module, name: name, version: params[:module_version])
+
+ ::Packages::CreatePackageFileService.new(package, file_params).execute
+
+ package
+ end
+
+ def current_package_exists_elsewhere?
+ ::Packages::Package
+ .for_projects(project.root_namespace.all_projects.id_not_in(project.id))
+ .with_package_type(:terraform_module)
+ .with_name(name)
+ .exists?
+ end
+
+ def current_package_version_exists?
+ project.packages
+ .with_package_type(:terraform_module)
+ .with_name(name)
+ .with_version(params[:module_version])
+ .exists?
+ end
+
+ def name
+ strong_memoize(:name) do
+ "#{params[:module_name]}/#{params[:module_system]}"
+ end
+ end
+
+ def file_name
+ strong_memoize(:file_name) do
+ "#{params[:module_name]}-#{params[:module_system]}-#{params[:module_version]}.tgz"
+ end
+ end
+
+ def file_params
+ {
+ file: params[:file],
+ size: params[:file].size,
+ file_sha256: params[:file].sha256,
+ file_name: file_name,
+ build: params[:build]
+ }
+ end
+
+ def file_size_exceeded?
+ project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size)
+ end
+ end
+ end
+end
diff --git a/app/services/pages/migrate_from_legacy_storage_service.rb b/app/services/pages/migrate_from_legacy_storage_service.rb
index b6aa08bba01..d102f93e863 100644
--- a/app/services/pages/migrate_from_legacy_storage_service.rb
+++ b/app/services/pages/migrate_from_legacy_storage_service.rb
@@ -59,7 +59,7 @@ module Pages
end
@logger.info(message: "Pages legacy storage migration: batch processed", migrated: @migrated, errored: @errored)
- rescue => e
+ rescue StandardError => e
# This method should never raise exception otherwise all threads might be killed
# and this will result in queue starving (and deadlock)
Gitlab::ErrorTracking.track_exception(e)
@@ -81,7 +81,7 @@ module Pages
@logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project.id, pages_path: project.pages_path, duration: time.round(2))
@counters_lock.synchronize { @errored += 1 }
end
- rescue => e
+ rescue StandardError => e
@counters_lock.synchronize { @errored += 1 }
@logger.error(message: "Pages legacy storage migration: project failed to be migrated: #{result[:message]}", project_id: project&.id, pages_path: project&.pages_path)
Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
diff --git a/app/services/pages/zip_directory_service.rb b/app/services/pages/zip_directory_service.rb
index 6cb79452e1b..895614a84a0 100644
--- a/app/services/pages/zip_directory_service.rb
+++ b/app/services/pages/zip_directory_service.rb
@@ -31,7 +31,7 @@ module Pages
end
success(archive_path: output_file, entries_count: entries_count)
- rescue => e
+ rescue StandardError => e
FileUtils.rm_f(output_file) if output_file
raise e
end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index e14241158a6..ca5df4ce017 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -66,7 +66,7 @@ module PagesDomains
project_id: pages_domain.project_id,
pages_domain: pages_domain.domain
)
- rescue => e
+ rescue StandardError => e
# getting authorizations is an additional network request which can raise errors
Gitlab::ErrorTracking.track_exception(e)
end
diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb
index 93a0135669f..7555ba26768 100644
--- a/app/services/personal_access_tokens/create_service.rb
+++ b/app/services/personal_access_tokens/create_service.rb
@@ -45,4 +45,4 @@ module PersonalAccessTokens
end
end
-PersonalAccessTokens::CreateService.prepend_if_ee('EE::PersonalAccessTokens::CreateService')
+PersonalAccessTokens::CreateService.prepend_mod_with('PersonalAccessTokens::CreateService')
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
index 34d542acab1..0275d03bcc9 100644
--- a/app/services/personal_access_tokens/revoke_service.rb
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -41,4 +41,4 @@ module PersonalAccessTokens
end
end
-PersonalAccessTokens::RevokeService.prepend_if_ee('EE::PersonalAccessTokens::RevokeService')
+PersonalAccessTokens::RevokeService.prepend_mod_with('PersonalAccessTokens::RevokeService')
diff --git a/app/services/pod_logs/elasticsearch_service.rb b/app/services/pod_logs/elasticsearch_service.rb
index 58d1bfbf835..28ccace62e5 100644
--- a/app/services/pod_logs/elasticsearch_service.rb
+++ b/app/services/pod_logs/elasticsearch_service.rb
@@ -24,7 +24,7 @@ module PodLogs
end
def get_raw_pods(result)
- client = cluster&.application_elastic_stack&.elasticsearch_client
+ client = cluster&.elasticsearch_client
return error(_('Unable to connect to Elasticsearch')) unless client
result[:raw_pods] = ::Gitlab::Elasticsearch::Logs::Pods.new(client).pods(namespace)
@@ -66,11 +66,9 @@ module PodLogs
end
def pod_logs(result)
- client = cluster&.application_elastic_stack&.elasticsearch_client
+ client = cluster&.elasticsearch_client
return error(_('Unable to connect to Elasticsearch')) unless client
- chart_above_v2 = cluster.application_elastic_stack.chart_above_v2?
-
response = ::Gitlab::Elasticsearch::Logs::Lines.new(client).pod_logs(
namespace,
pod_name: result[:pod_name],
@@ -79,7 +77,7 @@ module PodLogs
start_time: result[:start_time],
end_time: result[:end_time],
cursor: result[:cursor],
- chart_above_v2: chart_above_v2
+ chart_above_v2: cluster.elastic_stack_adapter.chart_above_v2?
)
result.merge!(response)
diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb
index 3dc8fd8929a..faacabbb16c 100644
--- a/app/services/post_receive_service.rb
+++ b/app/services/post_receive_service.rb
@@ -56,7 +56,7 @@ class PostReceiveService
end
service = ::MergeRequests::PushOptionsHandlerService.new(
- project, user, changes, push_options
+ project: project, current_user: user, changes: changes, push_options: push_options
).execute
if service.errors.present?
@@ -72,7 +72,7 @@ class PostReceiveService
def merge_request_urls
return [] unless repository&.repo_type&.project?
- ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+ ::MergeRequests::GetUrlsService.new(project: project).execute(params[:changes])
end
private
@@ -98,4 +98,4 @@ class PostReceiveService
end
end
-PostReceiveService.prepend_if_ee('EE::PostReceiveService')
+PostReceiveService.prepend_mod_with('PostReceiveService')
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index afe2651b11a..af9c338b59e 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -68,4 +68,4 @@ class PreviewMarkdownService < BaseService
end
end
-PreviewMarkdownService.prepend_if_ee('EE::PreviewMarkdownService')
+PreviewMarkdownService.prepend_mod_with('PreviewMarkdownService')
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index a2cdb87e631..6d389035922 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -49,10 +49,8 @@ module Projects
def first_ensure_no_registry_tags_are_present
return unless project.has_container_registry_tags?
- raise RenameFailedError.new(
- "Project #{full_path_before} cannot be renamed because images are " \
+ raise RenameFailedError, "Project #{full_path_before} cannot be renamed because images are " \
"present in its container registry"
- )
end
def expire_caches_before_rename
@@ -144,9 +142,9 @@ module Projects
Gitlab::AppLogger.error(error)
- raise RenameFailedError.new(error)
+ raise RenameFailedError, error
end
end
end
-Projects::AfterRenameService.prepend_if_ee('EE::Projects::AfterRenameService')
+Projects::AfterRenameService.prepend_mod_with('Projects::AfterRenameService')
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 68086f636b7..55f16aa3e3d 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -39,4 +39,4 @@ module Projects
end
end
-Projects::AutocompleteService.prepend_if_ee('EE::Projects::AutocompleteService')
+Projects::AutocompleteService.prepend_mod_with('Projects::AutocompleteService')
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 7bcaee75813..5eafa5f9b29 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -108,4 +108,4 @@ module Projects
end
end
-Projects::CleanupService.prepend_if_ee('EE::Projects::CleanupService')
+Projects::CleanupService.prepend_mod_with('Projects::CleanupService')
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index 3c66ff709c9..48dda09da71 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -58,4 +58,4 @@ module Projects
end
end
-Projects::CreateFromTemplateService.prepend_if_ee('EE::Projects::CreateFromTemplateService')
+Projects::CreateFromTemplateService.prepend_mod_with('Projects::CreateFromTemplateService')
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 5fb0bda912e..97ea7d87545 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -40,7 +40,7 @@ module Projects
if namespace_id
# Find matching namespace and check if it allowed
# for current user if namespace_id passed.
- unless allowed_namespace?(current_user, namespace_id)
+ unless current_user.can?(:create_projects, project_namespace)
@project.namespace_id = nil
deny_namespace
return @project
@@ -72,7 +72,7 @@ module Projects
rescue ActiveRecord::RecordInvalid => e
message = "Unable to save #{e.inspect}: #{e.record.errors.full_messages.join(", ")}"
fail(error: message)
- rescue => e
+ rescue StandardError => e
@project.errors.add(:base, e.message) if @project
fail(error: e.message)
end
@@ -83,13 +83,6 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
- # rubocop: disable CodeReuse/ActiveRecord
- def allowed_namespace?(user, namespace_id)
- namespace = Namespace.find_by(id: namespace_id)
- current_user.can?(:create_projects, namespace)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
@@ -156,7 +149,7 @@ module Projects
def create_readme
commit_attrs = {
- branch_name: @project.default_branch || 'master',
+ branch_name: @project.default_branch_or_main,
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"
@@ -174,7 +167,7 @@ module Projects
@project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save
- Service.create_from_active_default_integrations(@project, :project_id, with_templates: true)
+ Integration.create_from_active_default_integrations(@project, :project_id, with_templates: true)
@project.create_labels unless @project.gitlab_project_import?
@@ -271,7 +264,7 @@ module Projects
end
end
-Projects::CreateService.prepend_if_ee('EE::Projects::CreateService')
+Projects::CreateService.prepend_mod_with('Projects::CreateService')
# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well
Projects::CreateService.prepend(Measurable)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 4ba48f74273..0682f3013d4 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -41,7 +41,7 @@ module Projects
current_user.invalidate_personal_projects_count
true
- rescue => error
+ rescue StandardError => error
attempt_rollback(project, error.message)
false
rescue Exception => error # rubocop:disable Lint/RescueException
@@ -116,6 +116,7 @@ module Projects
log_destroy_event
trash_relation_repositories!
trash_project_repositories!
+ destroy_web_hooks! if Feature.enabled?(:destroy_webhooks_before_the_project, project, default_enabled: :yaml)
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
@@ -131,6 +132,23 @@ module Projects
log_info("Attempting to destroy #{project.full_path} (#{project.id})")
end
+ # The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
+ # By default, they are removed with "DELETE CASCADE" option defined via foreign_key.
+ # But such queries can exceed the statement_timeout limit and fail to delete the project.
+ # (see https://gitlab.com/gitlab-org/gitlab/-/issues/26259)
+ #
+ # To prevent that we use WebHooks::DestroyService. It deletes logs in batches and
+ # produces smaller and faster queries to the database.
+ def destroy_web_hooks!
+ project.hooks.find_each do |web_hook|
+ result = ::WebHooks::DestroyService.new(current_user).sync_destroy(web_hook)
+
+ unless result[:status] == :success
+ raise_error(s_('DeleteProject|Failed to remove webhooks. Please try again or contact administrator.'))
+ end
+ end
+ end
+
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
return false unless remove_legacy_registry_tags
@@ -156,7 +174,7 @@ module Projects
end
def raise_error(message)
- raise DestroyError.new(message)
+ raise DestroyError, message
end
def flush_caches(project)
@@ -165,4 +183,4 @@ module Projects
end
end
-Projects::DestroyService.prepend_if_ee('EE::Projects::DestroyService')
+Projects::DestroyService.prepend_mod_with('Projects::DestroyService')
diff --git a/app/services/projects/disable_deploy_key_service.rb b/app/services/projects/disable_deploy_key_service.rb
index 9fb2e3398b2..e0f309875de 100644
--- a/app/services/projects/disable_deploy_key_service.rb
+++ b/app/services/projects/disable_deploy_key_service.rb
@@ -12,4 +12,4 @@ module Projects
end
end
-Projects::DisableDeployKeyService.prepend_if_ee('EE::Projects::DisableDeployKeyService')
+Projects::DisableDeployKeyService.prepend_mod_with('Projects::DisableDeployKeyService')
diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb
index 0a24137bd61..581a6cc0ade 100644
--- a/app/services/projects/enable_deploy_key_service.rb
+++ b/app/services/projects/enable_deploy_key_service.rb
@@ -27,4 +27,4 @@ module Projects
end
end
-Projects::EnableDeployKeyService.prepend_if_ee('EE::Projects::EnableDeployKeyService')
+Projects::EnableDeployKeyService.prepend_mod_with('Projects::EnableDeployKeyService')
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
index 38f0e2f7c1a..63a41d172ea 100644
--- a/app/services/projects/gitlab_projects_import_service.rb
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -77,4 +77,4 @@ module Projects
end
end
-Projects::GitlabProjectsImportService.prepend_if_ee('EE::Projects::GitlabProjectsImportService')
+Projects::GitlabProjectsImportService.prepend_mod_with('Projects::GitlabProjectsImportService')
diff --git a/app/services/projects/group_links/create_service.rb b/app/services/projects/group_links/create_service.rb
index 3262839e246..d8fa2f36fcc 100644
--- a/app/services/projects/group_links/create_service.rb
+++ b/app/services/projects/group_links/create_service.rb
@@ -44,4 +44,4 @@ module Projects
end
end
-Projects::GroupLinks::CreateService.prepend_if_ee('EE::Projects::GroupLinks::CreateService')
+Projects::GroupLinks::CreateService.prepend_mod_with('Projects::GroupLinks::CreateService')
diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb
index 229191e41f6..bfe704cd780 100644
--- a/app/services/projects/group_links/destroy_service.rb
+++ b/app/services/projects/group_links/destroy_service.rb
@@ -20,4 +20,4 @@ module Projects
end
end
-Projects::GroupLinks::DestroyService.prepend_if_ee('EE::Projects::GroupLinks::DestroyService')
+Projects::GroupLinks::DestroyService.prepend_mod_with('Projects::GroupLinks::DestroyService')
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index 3d9d03c4a95..023f8494d99 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -64,4 +64,4 @@ module Projects
end
end
-Projects::HashedStorage::MigrateAttachmentsService.prepend_if_ee('EE::Projects::HashedStorage::MigrateAttachmentsService')
+Projects::HashedStorage::MigrateAttachmentsService.prepend_mod_with('Projects::HashedStorage::MigrateAttachmentsService')
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index adc7e38e4d5..c7989e04607 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -52,4 +52,4 @@ module Projects
end
end
-Projects::HashedStorage::MigrateRepositoryService.prepend_if_ee('EE::Projects::HashedStorage::MigrateRepositoryService')
+Projects::HashedStorage::MigrateRepositoryService.prepend_mod_with('Projects::HashedStorage::MigrateRepositoryService')
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
deleted file mode 100644
index b5589d556aa..00000000000
--- a/app/services/projects/housekeeping_service.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# This is a compatibility class to avoid calling a non-existent
-# class from sidekiq during deployment.
-#
-# We're deploying the rename of this class in 13.9. Nevertheless,
-# we cannot remove this class entirely because there can be jobs
-# referencing it.
-#
-# We can get rid of this class in 13.10
-# https://gitlab.com/gitlab-org/gitlab/-/issues/297580
-#
-module Projects
- class HousekeepingService < ::Repositories::HousekeepingService
- end
-end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index c2a8db7b657..64c0f1ff4ac 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -112,7 +112,7 @@ module Projects
def notify_error!
notify_error
- raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
+ raise Gitlab::ImportExport::Error, shared.errors.to_sentence
end
def notify_success
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index b4abb5b6df7..b5288aad6f0 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -29,7 +29,7 @@ module Projects
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: e.message })
- rescue => e
+ rescue StandardError => e
message = Projects::ImportErrorFilter.filter_message(e.message)
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
@@ -149,7 +149,7 @@ module Projects
end
end
-Projects::ImportService.prepend_if_ee('EE::Projects::ImportService')
+Projects::ImportService.prepend_mod_with('Projects::ImportService')
# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well
Projects::ImportService.prepend(Measurable)
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
index 2afcce7099b..3fc82f2c410 100644
--- a/app/services/projects/lfs_pointers/lfs_import_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -16,7 +16,7 @@ module Projects
end
success
- rescue => e
+ rescue StandardError => e
error(e.message)
end
end
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index 7dfe7fffa1b..c0734171ee5 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -135,4 +135,4 @@ module Projects
end
end
-Projects::Operations::UpdateService.prepend_if_ee('::EE::Projects::Operations::UpdateService')
+Projects::Operations::UpdateService.prepend_mod_with('Projects::Operations::UpdateService')
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 93165a58470..db640a54745 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -63,7 +63,7 @@ module Projects
def valid_alert_manager_token?(token, integration)
valid_for_manual?(token) ||
valid_for_alerts_endpoint?(token, integration) ||
- valid_for_managed?(token)
+ valid_for_cluster?(token)
end
def valid_for_manual?(token)
@@ -83,18 +83,20 @@ module Projects
compare_token(token, integration.token)
end
- def valid_for_managed?(token)
- prometheus_application = available_prometheus_application(project)
- return false unless prometheus_application
+ def valid_for_cluster?(token)
+ cluster_integration = find_cluster_integration(project)
+ return false unless cluster_integration
+
+ cluster_integration_token = cluster_integration.alert_manager_token
if token
- compare_token(token, prometheus_application.alert_manager_token)
+ compare_token(token, cluster_integration_token)
else
- prometheus_application.alert_manager_token.nil?
+ cluster_integration_token.nil?
end
end
- def available_prometheus_application(project)
+ def find_cluster_integration(project)
alert_id = gitlab_alert_id
return unless alert_id
@@ -105,7 +107,7 @@ module Projects
return unless cluster&.enabled?
return unless cluster.application_prometheus_available?
- cluster.application_prometheus
+ cluster.application_prometheus || cluster.integration_prometheus
end
def find_alert(project, metric)
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 8a5e0706126..d9e49dfae61 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -47,16 +47,16 @@ module Projects
@old_namespace = project.namespace
if Project.where(namespace_id: @new_namespace.try(:id)).where('path = ? or name = ?', project.path, project.name).exists?
- raise TransferError.new(s_("TransferProject|Project with same name or path in target namespace already exists"))
+ raise TransferError, s_("TransferProject|Project with same name or path in target namespace already exists")
end
if project.has_container_registry_tags?
# We currently don't support renaming repository if it contains tags in container registry
- raise TransferError.new(s_('TransferProject|Project cannot be transferred, because tags are present in its container registry'))
+ raise TransferError, s_('TransferProject|Project cannot be transferred, because tags are present in its container registry')
end
if project.has_packages?(:npm) && !new_namespace_has_same_root?(project)
- raise TransferError.new(s_("TransferProject|Root namespace can't be updated if project has NPM packages"))
+ raise TransferError, s_("TransferProject|Root namespace can't be updated if project has NPM packages")
end
proceed_to_transfer
@@ -170,7 +170,7 @@ module Projects
# Move main repository
unless move_repo_folder(@old_path, @new_path)
- raise TransferError.new(s_("TransferProject|Cannot move project"))
+ raise TransferError, s_("TransferProject|Cannot move project")
end
# Disk path is changed; we need to ensure we reload it
@@ -223,10 +223,10 @@ module Projects
end
def update_integrations
- project.services.inherit.delete_all
- Service.create_from_active_default_integrations(project, :project_id)
+ project.integrations.inherit.delete_all
+ Integration.create_from_active_default_integrations(project, :project_id)
end
end
end
-Projects::TransferService.prepend_if_ee('EE::Projects::TransferService')
+Projects::TransferService.prepend_mod_with('Projects::TransferService')
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index 91632e50ba8..9eccc16a8b2 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -17,7 +17,7 @@ module Projects
.from_and_to_forks(@project)
merge_requests.find_each do |mr|
- ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
+ ::MergeRequests::CloseService.new(project: @project, current_user: @current_user).execute(mr)
log_info(message: "UnlinkForkService: Closed merge request", merge_request_id: mr.id)
end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index b63903c6c61..4272e1dc8b6 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -102,7 +102,7 @@ module Projects
File.open(file, 'r') do |f|
f.read
end
- rescue
+ rescue StandardError
nil
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 6fa42b293c5..8ea35131339 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -48,7 +48,7 @@ module Projects
end
rescue InvalidStateError => e
error(e.message)
- rescue => e
+ rescue StandardError => e
error(e.message)
raise e
end
@@ -145,7 +145,7 @@ module Projects
FileUtils.mkdir_p(pages_path)
begin
FileUtils.move(public_path, previous_public_path)
- rescue
+ rescue StandardError
end
FileUtils.move(archive_public_path, public_path)
ensure
@@ -267,4 +267,4 @@ module Projects
end
end
-Projects::UpdatePagesService.prepend_if_ee('EE::Projects::UpdatePagesService')
+Projects::UpdatePagesService.prepend_mod_with('Projects::UpdatePagesService')
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 8832a1bc027..9f4f6133d92 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -24,7 +24,7 @@ module Projects
hard_retry_or_fail(remote_mirror, e.message, tries)
error(e.message)
- rescue => e
+ rescue StandardError => e
remote_mirror.hard_fail!(e.message)
raise e
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 8384bfa813f..541b333aae3 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -49,11 +49,11 @@ module Projects
def validate!
unless valid_visibility_level_change?(project, params[:visibility_level])
- raise ValidationError.new(s_('UpdateProject|New visibility level not allowed!'))
+ raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
end
if renaming_project_with_container_registry_tags?
- raise ValidationError.new(s_('UpdateProject|Cannot rename project because it contains container registry tags!'))
+ raise ValidationError, s_('UpdateProject|Cannot rename project because it contains container registry tags!')
end
validate_default_branch_change
@@ -67,7 +67,7 @@ module Projects
if project.change_head(params[:default_branch])
after_default_branch_change(previous_default_branch)
else
- raise ValidationError.new(s_("UpdateProject|Could not set the default branch"))
+ raise ValidationError, s_("UpdateProject|Could not set the default branch")
end
end
@@ -170,4 +170,4 @@ module Projects
end
end
-Projects::UpdateService.prepend_if_ee('EE::Projects::UpdateService')
+Projects::UpdateService.prepend_mod_with('Projects::UpdateService')
diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
index a0793cff2df..71f5a8e633d 100644
--- a/app/services/projects/update_statistics_service.rb
+++ b/app/services/projects/update_statistics_service.rb
@@ -2,18 +2,49 @@
module Projects
class UpdateStatisticsService < BaseService
+ include ::Gitlab::Utils::StrongMemoize
+
+ STAT_TO_CACHED_METHOD = {
+ repository_size: :size,
+ commit_count: :commit_count
+ }.freeze
+
def execute
return unless project
Gitlab::AppLogger.info("Updating statistics for project #{project.id}")
- project.statistics.refresh!(only: statistics.map(&:to_sym))
+ expire_repository_caches
+ expire_wiki_caches
+ project.statistics.refresh!(only: statistics)
end
private
+ def expire_repository_caches
+ if statistics.empty?
+ project.repository.expire_statistics_caches
+ elsif method_caches_to_expire.present?
+ project.repository.expire_method_caches(method_caches_to_expire)
+ end
+ end
+
+ def expire_wiki_caches
+ return unless project.wiki_enabled? && statistics.include?(:wiki_size)
+
+ project.wiki.repository.expire_method_caches([:size])
+ end
+
+ def method_caches_to_expire
+ strong_memoize(:method_caches_to_expire) do
+ statistics.map { |stat| STAT_TO_CACHED_METHOD[stat] }.compact
+ end
+ end
+
def statistics
- params[:statistics]
+ strong_memoize(:statistics) do
+ params[:statistics]&.map(&:to_sym)
+ end
end
end
end
diff --git a/app/services/prometheus/create_default_alerts_service.rb b/app/services/prometheus/create_default_alerts_service.rb
index 4ae2743cc28..e59b0a8e8e3 100644
--- a/app/services/prometheus/create_default_alerts_service.rb
+++ b/app/services/prometheus/create_default_alerts_service.rb
@@ -84,7 +84,7 @@ module Prometheus
def environment
strong_memoize(:environment) do
- EnvironmentsFinder.new(project, nil, name: 'production').execute.first ||
+ Environments::EnvironmentsFinder.new(project, nil, name: 'production').execute.first ||
project.environments.first
end
end
diff --git a/app/services/protected_branches/access_level_params.rb b/app/services/protected_branches/access_level_params.rb
index e34bc23b4dc..6f7a289d9b4 100644
--- a/app/services/protected_branches/access_level_params.rb
+++ b/app/services/protected_branches/access_level_params.rb
@@ -34,4 +34,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::AccessLevelParams.prepend_if_ee('EE::ProtectedBranches::AccessLevelParams')
+ProtectedBranches::AccessLevelParams.prepend_mod_with('ProtectedBranches::AccessLevelParams')
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index bf1a966472b..3e5122a1523 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -21,4 +21,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::ApiService.prepend_if_ee('EE::ProtectedBranches::ApiService')
+ProtectedBranches::ApiService.prepend_mod_with('ProtectedBranches::ApiService')
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 3c86d7d087d..37083a4a9e4 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -26,4 +26,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::CreateService.prepend_if_ee('EE::ProtectedBranches::CreateService')
+ProtectedBranches::CreateService.prepend_mod_with('ProtectedBranches::CreateService')
diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb
index acd15b0214f..dc177f0ac09 100644
--- a/app/services/protected_branches/destroy_service.rb
+++ b/app/services/protected_branches/destroy_service.rb
@@ -10,4 +10,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::DestroyService.prepend_if_ee('EE::ProtectedBranches::DestroyService')
+ProtectedBranches::DestroyService.prepend_mod_with('ProtectedBranches::DestroyService')
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index 0cad23f20f7..8ff6c4bd734 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -49,4 +49,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::LegacyApiUpdateService.prepend_if_ee('EE::ProtectedBranches::LegacyApiUpdateService')
+ProtectedBranches::LegacyApiUpdateService.prepend_mod_with('ProtectedBranches::LegacyApiUpdateService')
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index 226aefb64d0..1815d92421e 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -11,4 +11,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::UpdateService.prepend_if_ee('EE::ProtectedBranches::UpdateService')
+ProtectedBranches::UpdateService.prepend_mod_with('ProtectedBranches::UpdateService')
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index ea90d8e3dd8..ab489ba49ca 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -190,4 +190,4 @@ module QuickActions
end
end
-QuickActions::InterpretService.prepend_if_ee('EE::QuickActions::InterpretService')
+QuickActions::InterpretService.prepend_mod_with('QuickActions::InterpretService')
diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb
index a465632ccfb..6eda3c89e6c 100644
--- a/app/services/quick_actions/target_service.rb
+++ b/app/services/quick_actions/target_service.rb
@@ -37,4 +37,4 @@ module QuickActions
end
end
-QuickActions::TargetService.prepend_if_ee('EE::QuickActions::TargetService')
+QuickActions::TargetService.prepend_mod_with('QuickActions::TargetService')
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index de7c97b3518..9dd0c9a007a 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -86,4 +86,4 @@ module Releases
end
end
-Releases::BaseService.prepend_if_ee('EE::Releases::BaseService')
+Releases::BaseService.prepend_mod_with('Releases::BaseService')
diff --git a/app/services/releases/create_evidence_service.rb b/app/services/releases/create_evidence_service.rb
index 78b6d77c2cb..64cce45e400 100644
--- a/app/services/releases/create_evidence_service.rb
+++ b/app/services/releases/create_evidence_service.rb
@@ -28,4 +28,4 @@ module Releases
end
end
-Releases::CreateEvidenceService.prepend_if_ee('EE::Releases::CreateEvidenceService')
+Releases::CreateEvidenceService.prepend_mod_with('Releases::CreateEvidenceService')
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 11fdbaf3169..1096e207e02 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -57,7 +57,7 @@ module Releases
create_evidence!(release, evidence_pipeline)
success(tag: tag, release: release)
- rescue => e
+ rescue StandardError => e
error(e.message, 400)
end
diff --git a/app/services/repositories/changelog_service.rb b/app/services/repositories/changelog_service.rb
index 0122bfb154d..bac3fdf36da 100644
--- a/app/services/repositories/changelog_service.rb
+++ b/app/services/repositories/changelog_service.rb
@@ -39,7 +39,7 @@ module Repositories
project,
user,
version:,
- branch: project.default_branch_or_master,
+ branch: project.default_branch_or_main,
from: nil,
to: branch,
date: DateTime.now,
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 620dfff91e2..84f4478f20f 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -120,4 +120,4 @@ module ResourceAccessTokens
end
end
-ResourceAccessTokens::CreateService.prepend_if_ee('EE::ResourceAccessTokens::CreateService')
+ResourceAccessTokens::CreateService.prepend_mod_with('ResourceAccessTokens::CreateService')
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
index 0924ca3bac4..9543ea4b68d 100644
--- a/app/services/resource_access_tokens/revoke_service.rb
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -67,4 +67,4 @@ module ResourceAccessTokens
end
end
-ResourceAccessTokens::RevokeService.prepend_if_ee('EE::ResourceAccessTokens::RevokeService')
+ResourceAccessTokens::RevokeService.prepend_mod_with('ResourceAccessTokens::RevokeService')
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 89eb90e9360..3797d41a5df 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -44,4 +44,4 @@ module ResourceEvents
end
end
-ResourceEvents::ChangeLabelsService.prepend_if_ee('EE::ResourceEvents::ChangeLabelsService')
+ResourceEvents::ChangeLabelsService.prepend_mod_with('ResourceEvents::ChangeLabelsService')
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index 122bcb8550f..ea465c1e75e 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -37,4 +37,4 @@ module ResourceEvents
end
end
-ResourceEvents::MergeIntoNotesService.prepend_if_ee('EE::ResourceEvents::MergeIntoNotesService')
+ResourceEvents::MergeIntoNotesService.prepend_mod_with('ResourceEvents::MergeIntoNotesService')
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 055034d87a1..661aafc70cd 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -38,4 +38,4 @@ module Search
end
end
-Search::GlobalService.prepend_if_ee('EE::Search::GlobalService')
+Search::GlobalService.prepend_mod_with('Search::GlobalService')
diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb
index 4b2d8499582..daed0df83f3 100644
--- a/app/services/search/group_service.rb
+++ b/app/services/search/group_service.rb
@@ -31,4 +31,4 @@ module Search
end
end
-Search::GroupService.prepend_if_ee('EE::Search::GroupService')
+Search::GroupService.prepend_mod_with('Search::GroupService')
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 4227dfe2fac..3181c0098cc 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -37,4 +37,4 @@ module Search
end
end
-Search::ProjectService.prepend_if_ee('EE::Search::ProjectService')
+Search::ProjectService.prepend_mod_with('Search::ProjectService')
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 30401b28571..b629fd305d7 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -12,4 +12,4 @@ module Search
end
end
-Search::SnippetService.prepend_if_ee('::EE::Search::SnippetService')
+Search::SnippetService.prepend_mod_with('Search::SnippetService')
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 84d7e33c3d0..389cf17e115 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -140,4 +140,4 @@ class SearchService
attr_reader :current_user, :params
end
-SearchService.prepend_if_ee('EE::SearchService')
+SearchService.prepend_mod_with('SearchService')
diff --git a/app/services/security/ci_configuration/base_create_service.rb b/app/services/security/ci_configuration/base_create_service.rb
new file mode 100644
index 00000000000..adb45244adb
--- /dev/null
+++ b/app/services/security/ci_configuration/base_create_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ class BaseCreateService
+ attr_reader :branch_name, :current_user, :project
+
+ def initialize(project, current_user)
+ @project = project
+ @current_user = current_user
+ @branch_name = project.repository.next_branch(next_branch)
+ end
+
+ def execute
+ project.repository.add_branch(current_user, branch_name, project.default_branch)
+
+ attributes_for_commit = attributes
+
+ result = ::Files::MultiService.new(project, current_user, attributes_for_commit).execute
+
+ return ServiceResponse.error(message: result[:message]) unless result[:status] == :success
+
+ track_event(attributes_for_commit)
+ ServiceResponse.success(payload: { branch: branch_name, success_path: successful_change_path })
+ rescue Gitlab::Git::PreReceiveError => e
+ ServiceResponse.error(message: e.message)
+ rescue StandardError
+ project.repository.rm_branch(current_user, branch_name) if project.repository.branch_exists?(branch_name)
+ raise
+ end
+
+ private
+
+ def attributes
+ {
+ commit_message: message,
+ branch_name: branch_name,
+ start_branch: branch_name,
+ actions: [action]
+ }
+ end
+
+ def existing_gitlab_ci_content
+ @gitlab_ci_yml ||= project.repository.gitlab_ci_yml_for(project.repository.root_ref_sha)
+ YAML.safe_load(@gitlab_ci_yml) if @gitlab_ci_yml
+ end
+
+ def successful_change_path
+ merge_request_params = { source_branch: branch_name, description: description }
+ Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
+ end
+
+ def track_event(attributes_for_commit)
+ action = attributes_for_commit[:actions].first
+
+ Gitlab::Tracking.event(
+ self.class.to_s, action[:action], label: action[:default_values_overwritten].to_s
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/security/ci_configuration/sast_create_service.rb b/app/services/security/ci_configuration/sast_create_service.rb
index 8fc3b8d078c..f495cac18f8 100644
--- a/app/services/security/ci_configuration/sast_create_service.rb
+++ b/app/services/security/ci_configuration/sast_create_service.rb
@@ -2,64 +2,30 @@
module Security
module CiConfiguration
- class SastCreateService < ::BaseService
+ class SastCreateService < ::Security::CiConfiguration::BaseCreateService
+ attr_reader :params
+
def initialize(project, current_user, params)
- @project = project
- @current_user = current_user
+ super(project, current_user)
@params = params
- @branch_name = @project.repository.next_branch('set-sast-config')
- end
-
- def execute
- attributes_for_commit = attributes
- result = ::Files::MultiService.new(@project, @current_user, attributes_for_commit).execute
-
- if result[:status] == :success
- result[:success_path] = successful_change_path
- track_event(attributes_for_commit)
- else
- result[:errors] = result[:message]
- end
-
- result
-
- rescue Gitlab::Git::PreReceiveError => e
- { status: :error, errors: e.message }
end
private
- def attributes
- actions = Security::CiConfiguration::SastBuildActions.new(@project.auto_devops_enabled?, @params, existing_gitlab_ci_content).generate
-
- @project.repository.add_branch(@current_user, @branch_name, @project.default_branch)
- message = _('Set .gitlab-ci.yml to enable or configure SAST')
-
- {
- commit_message: message,
- branch_name: @branch_name,
- start_branch: @branch_name,
- actions: actions
- }
+ def action
+ Security::CiConfiguration::SastBuildAction.new(project.auto_devops_enabled?, params, existing_gitlab_ci_content).generate
end
- def existing_gitlab_ci_content
- gitlab_ci_yml = @project.repository.gitlab_ci_yml_for(@project.repository.root_ref_sha)
- YAML.safe_load(gitlab_ci_yml) if gitlab_ci_yml
+ def next_branch
+ 'set-sast-config'
end
- def successful_change_path
- description = _('Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.')
- merge_request_params = { source_branch: @branch_name, description: description }
- Gitlab::Routing.url_helpers.project_new_merge_request_url(@project, merge_request: merge_request_params)
+ def message
+ _('Configure SAST in `.gitlab-ci.yml`, creating this file if it does not already exist')
end
- def track_event(attributes_for_commit)
- action = attributes_for_commit[:actions].first
-
- Gitlab::Tracking.event(
- self.class.to_s, action[:action], label: action[:default_values_overwritten].to_s
- )
+ def description
+ _('Configure SAST in `.gitlab-ci.yml` using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.')
end
end
end
diff --git a/app/services/security/ci_configuration/sast_parser_service.rb b/app/services/security/ci_configuration/sast_parser_service.rb
index a8fe5764d19..5220525d552 100644
--- a/app/services/security/ci_configuration/sast_parser_service.rb
+++ b/app/services/security/ci_configuration/sast_parser_service.rb
@@ -74,7 +74,7 @@ module Security
def sast_excluded_analyzers
strong_memoize(:sast_excluded_analyzers) do
- all_analyzers = Security::CiConfiguration::SastBuildActions::SAST_DEFAULT_ANALYZERS.split(', ') rescue []
+ all_analyzers = Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS.split(', ') rescue []
enabled_analyzers = sast_default_analyzers.split(',').map(&:strip) rescue []
excluded_analyzers = gitlab_ci_yml_attributes["SAST_EXCLUDED_ANALYZERS"] || sast_template_attributes["SAST_EXCLUDED_ANALYZERS"]
diff --git a/app/services/security/ci_configuration/secret_detection_create_service.rb b/app/services/security/ci_configuration/secret_detection_create_service.rb
new file mode 100644
index 00000000000..ff3458d36fc
--- /dev/null
+++ b/app/services/security/ci_configuration/secret_detection_create_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Security
+ module CiConfiguration
+ class SecretDetectionCreateService < ::Security::CiConfiguration::BaseCreateService
+ private
+
+ def action
+ Security::CiConfiguration::SecretDetectionBuildAction.new(project.auto_devops_enabled?, existing_gitlab_ci_content).generate
+ end
+
+ def next_branch
+ 'set-secret-detection-config'
+ end
+
+ def message
+ _('Configure Secret Detection in `.gitlab-ci.yml`, creating this file if it does not already exist')
+ end
+
+ def description
+ _('Configure Secret Detection in `.gitlab-ci.yml` using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings) to customize Secret Detection settings.')
+ end
+ end
+ end
+end
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index 74c0be22d46..6bc394d2ae2 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -18,6 +18,14 @@ class ServiceResponse
self.http_status = http_status
end
+ def [](key)
+ to_h[key]
+ end
+
+ def to_h
+ (payload || {}).merge(status: status, message: message, http_status: http_status)
+ end
+
def success?
status == :success
end
diff --git a/app/services/snippets/bulk_destroy_service.rb b/app/services/snippets/bulk_destroy_service.rb
index a612d8f8dfc..430e8330b59 100644
--- a/app/services/snippets/bulk_destroy_service.rb
+++ b/app/services/snippets/bulk_destroy_service.rb
@@ -27,7 +27,7 @@ module Snippets
rescue DeleteRepositoryError
attempt_rollback_repositories
service_response_error('Failed to delete snippet repositories.', 400)
- rescue
+ rescue StandardError
# In case the delete operation fails
attempt_rollback_repositories
service_response_error('Failed to remove snippets.', 400)
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index c95b459cd2a..aadf9b865b8 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -69,7 +69,7 @@ module Snippets
end
snippet_saved
- rescue => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ...
+ rescue StandardError => e # Rescuing all because we can receive Creation exceptions, GRPC exceptions, Git exceptions, ...
Gitlab::ErrorTracking.log_exception(e, service: 'Snippets::CreateService')
# If the commit action failed we need to remove the repository if exists
diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb
index f1f80dbaf86..96157434462 100644
--- a/app/services/snippets/destroy_service.rb
+++ b/app/services/snippets/destroy_service.rb
@@ -30,7 +30,7 @@ module Snippets
ServiceResponse.success(message: 'Snippet was deleted.')
rescue DestroyError
service_response_error('Failed to remove snippet repository.', 400)
- rescue
+ rescue StandardError
attempt_rollback_repository
service_response_error('Failed to remove snippet.', 400)
end
@@ -59,4 +59,4 @@ module Snippets
end
end
-Snippets::DestroyService.prepend_if_ee('EE::Snippets::DestroyService')
+Snippets::DestroyService.prepend_mod_with('Snippets::DestroyService')
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index aedb6a4819d..4088a08272d 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -78,7 +78,7 @@ module Snippets
create_commit(snippet)
true
- rescue => e
+ rescue StandardError => e
# Restore old attributes but re-assign changes so they're not lost
unless snippet.previous_changes.empty?
snippet.previous_changes.each { |attr, value| snippet[attr] = value[0] }
diff --git a/app/services/spam/akismet_service.rb b/app/services/spam/akismet_service.rb
index e11a1dbdd96..4e56972ccd5 100644
--- a/app/services/spam/akismet_service.rb
+++ b/app/services/spam/akismet_service.rb
@@ -20,13 +20,13 @@ module Spam
created_at: DateTime.current,
author: owner_name,
author_email: owner_email,
- referrer: options[:referrer]
+ referer: options[:referer]
}
begin
is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
is_spam || is_blatant
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error("Unable to connect to Akismet: #{e}, skipping check")
false
end
@@ -66,7 +66,7 @@ module Spam
begin
akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend
true
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error("Unable to connect to Akismet: #{e}, skipping!")
false
end
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index 2220198583c..3ae5111b994 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -53,7 +53,7 @@ module Spam
if request
options[:ip_address] = request.env['action_dispatch.remote_ip'].to_s
options[:user_agent] = request.env['HTTP_USER_AGENT']
- options[:referrer] = request.env['HTTP_REFERRER']
+ options[:referer] = request.env['HTTP_REFERER']
else
# TODO: This code is never used, because we do not perform a verification if there is not a
# request. Why? Should it be deleted? Or should we check even if there is no request?
@@ -123,8 +123,16 @@ module Spam
# https://gitlab.com/gitlab-org/gitlab/-/issues/214739
target.spam! unless target.allow_possible_spam?
create_spam_log(api)
+ when BLOCK_USER
+ # TODO: improve BLOCK_USER handling, non-existent until now
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/329666
+ target.spam! unless target.allow_possible_spam?
+ create_spam_log(api)
when ALLOW
target.clear_spam_flags!
+ when NOOP
+ # spamcheck is not explicitly rendering a verdict & therefore can't make a decision
+ target.clear_spam_flags!
end
end
end
diff --git a/app/services/spam/spam_constants.rb b/app/services/spam/spam_constants.rb
index 2a16cfae78b..b654fbbbcc8 100644
--- a/app/services/spam/spam_constants.rb
+++ b/app/services/spam/spam_constants.rb
@@ -6,6 +6,7 @@ module Spam
DISALLOW = "disallow"
ALLOW = "allow"
BLOCK_USER = "block"
+ NOOP = "noop"
SUPPORTED_VERDICTS = {
BLOCK_USER => {
@@ -19,6 +20,9 @@ module Spam
},
ALLOW => {
priority: 4
+ },
+ NOOP => {
+ priority: 5
}
}.freeze
end
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
index 7de3bad607a..7155017b73f 100644
--- a/app/services/spam/spam_verdict_service.rb
+++ b/app/services/spam/spam_verdict_service.rb
@@ -10,25 +10,56 @@ module Spam
@request = request
@user = user
@options = options
- @verdict_params = assemble_verdict_params(context)
+ @context = context
end
def execute
- external_spam_check_result = external_verdict
+ spamcheck_result = nil
+ spamcheck_attribs = {}
+ spamcheck_error = false
+
+ external_spam_check_round_trip_time = Benchmark.realtime do
+ spamcheck_result, spamcheck_attribs, spamcheck_error = spamcheck_verdict
+ end
+
+ label = spamcheck_error ? 'ERROR' : spamcheck_result.to_s.upcase
+
+ histogram.observe( { result: label }, external_spam_check_round_trip_time )
+
+ # assign result to a var for logging it before reassigning to nil when monitorMode is true
+ original_spamcheck_result = spamcheck_result
+
+ spamcheck_result = nil if spamcheck_attribs&.fetch("monitorMode", "false") == "true"
+
akismet_result = akismet_verdict
# filter out anything we don't recognise, including nils.
- valid_results = [external_spam_check_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) }
+ valid_results = [spamcheck_result, akismet_result].compact.select { |r| SUPPORTED_VERDICTS.key?(r) }
+
# Treat nils - such as service unavailable - as ALLOW
return ALLOW unless valid_results.any?
# Favour the most restrictive result.
- valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] }
+ final_verdict = valid_results.min_by { |v| SUPPORTED_VERDICTS[v][:priority] }
+
+ logger.info(class: self.class.name,
+ akismet_verdict: akismet_verdict,
+ spam_check_verdict: original_spamcheck_result,
+ extra_attributes: spamcheck_attribs,
+ spam_check_rtt: external_spam_check_round_trip_time.real,
+ final_verdict: final_verdict,
+ username: user.username,
+ user_id: user.id,
+ target_type: target.class.to_s,
+ project_id: target.project_id
+ )
+
+ final_verdict
end
private
- attr_reader :user, :target, :request, :options, :verdict_params
+ attr_reader :user, :target, :request, :options, :context
def akismet_verdict
if akismet.spam?
@@ -38,54 +69,41 @@ module Spam
end
end
- def external_verdict
+ def spamcheck_verdict
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
- return if endpoint_url.blank?
begin
- result = Gitlab::HTTP.post(endpoint_url, body: verdict_params.to_json, headers: { 'Content-Type' => 'application/json' })
- return unless result
-
- json_result = Gitlab::Json.parse(result).with_indifferent_access
- # @TODO metrics/logging
- # Expecting:
- # error: (string or nil)
- # verdict: (string or nil)
- # @TODO log if json_result[:error]
-
- json_result[:verdict]
- rescue *Gitlab::HTTP::HTTP_ERRORS => e
- # @TODO: log error via try_post https://gitlab.com/gitlab-org/gitlab/-/issues/219223
+ result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
+ return [nil, attribs] unless result
+
+ # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
+
+ return [result, attribs] if result == NOOP || attribs["monitorMode"] == "true"
+
+ # Duplicate logic with Akismet logic in #akismet_verdict
+ if Gitlab::Recaptcha.enabled? && result != ALLOW
+ [CONDITIONAL_ALLOW, attribs]
+ else
+ [result, attribs]
+ end
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
- nil
- rescue
- # @TODO log
- ALLOW
+
+ # Default to ALLOW if any errors occur
+ [ALLOW, attribs, true]
end
end
- def assemble_verdict_params(context)
- return {} unless endpoint_url.present?
-
- project = target.try(:project)
-
- context.merge({
- target: {
- title: target.spam_title,
- description: target.spam_description,
- type: target.class.to_s
- },
- user: {
- created_at: user.created_at,
- email: user.email,
- username: user.username
- },
- user_in_project: user.authorized_project?(project)
- })
+ def spamcheck_client
+ @spamcheck_client ||= Gitlab::Spamcheck::Client.new
+ end
+
+ def logger
+ @logger ||= Gitlab::AppJsonLogger.build
end
- def endpoint_url
- @endpoint_url ||= Gitlab::CurrentSettings.current_application_settings.spam_check_endpoint_url
+ def histogram
+ @histogram ||= Gitlab::Metrics.histogram(:gitlab_spamcheck_request_duration_seconds, 'Request duration to the anti-spam service')
end
end
end
diff --git a/app/services/static_site_editor/config_service.rb b/app/services/static_site_editor/config_service.rb
index 7b3115468a5..c8e7165e076 100644
--- a/app/services/static_site_editor/config_service.rb
+++ b/app/services/static_site_editor/config_service.rb
@@ -25,7 +25,7 @@ module StaticSiteEditor
ServiceResponse.success(payload: data)
rescue ValidationError => e
ServiceResponse.error(message: e.message)
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_and_raise_exception(e)
end
@@ -67,7 +67,7 @@ module StaticSiteEditor
def check_for_duplicate_keys!(generated_data, file_data)
duplicate_keys = generated_data.keys & file_data.keys
- raise ValidationError.new("Duplicate key(s) '#{duplicate_keys}' found.") if duplicate_keys.present?
+ raise ValidationError, "Duplicate key(s) '#{duplicate_keys}' found." if duplicate_keys.present?
end
def merged_data(generated_data, file_data)
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index d628b1ea7c7..4942dd0e913 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -22,7 +22,7 @@ class SubmitUsagePingService
usage_data = Gitlab::UsageData.data(force_refresh: true)
- raise SubmissionError.new('Usage data is blank') if usage_data.blank?
+ raise SubmissionError, 'Usage data is blank' if usage_data.blank?
raw_usage_data = save_raw_usage_data(usage_data)
@@ -33,12 +33,12 @@ class SubmitUsagePingService
headers: { 'Content-type' => 'application/json' }
)
- raise SubmissionError.new("Unsuccessful response code: #{response.code}") unless response.success?
+ raise SubmissionError, "Unsuccessful response code: #{response.code}" unless response.success?
version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
unless version_usage_data_id.is_a?(Integer) && version_usage_data_id > 0
- raise SubmissionError.new("Invalid usage_data_id in response: #{version_usage_data_id}")
+ raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
end
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
@@ -73,3 +73,5 @@ class SubmitUsagePingService
end
end
end
+
+SubmitUsagePingService.prepend_mod
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
index f9783f4271f..6836700a67d 100644
--- a/app/services/suggestions/apply_service.rb
+++ b/app/services/suggestions/apply_service.rb
@@ -36,12 +36,22 @@ module Suggestions
.track_apply_suggestion_action(user: current_user)
end
+ def author
+ authors = suggestion_set.authors
+
+ return unless authors.one?
+
+ Gitlab::Git::User.from_gitlab(authors.first)
+ end
+
def multi_service
params = {
commit_message: commit_message,
branch_name: suggestion_set.branch,
start_branch: suggestion_set.branch,
- actions: suggestion_set.actions
+ actions: suggestion_set.actions,
+ author_name: author&.name,
+ author_email: author&.email
}
::Files::MultiService.new(suggestion_set.project, current_user, params)
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 53e810035c5..2a2053cb912 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class SystemHooksService
- BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES = [GroupMember, Group, ProjectMember, User].freeze
-
def execute_hooks_for(model, event)
data = build_event_data(model, event)
@@ -12,7 +10,7 @@ class SystemHooksService
end
def execute_hooks(data, hooks_scope = :all)
- SystemHook.hooks_for(hooks_scope).find_each do |hook|
+ SystemHook.executable.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks')
end
@@ -22,59 +20,6 @@ class SystemHooksService
private
def build_event_data(model, event)
- # return entire event data from its builder class, if available.
- return builder_driven_event_data(model, event) if builder_driven_event_data_available?(model)
-
- data = {
- event_name: build_event_name(model, event),
- created_at: model.created_at&.xmlschema,
- updated_at: model.updated_at&.xmlschema
- }
-
- case model
- when Key
- data.merge!(
- key: model.key,
- id: model.id
- )
-
- if model.user
- data[:username] = model.user.username
- end
- when Project
- data.merge!(project_data(model))
-
- if event == :rename || event == :transfer
- data[:old_path_with_namespace] = model.old_path_with_namespace
- end
- end
-
- data
- end
-
- def build_event_name(model, event)
- "#{model.class.name.downcase}_#{event}"
- end
-
- def project_data(model)
- owner = model.owner
-
- {
- name: model.name,
- path: model.path,
- path_with_namespace: model.full_path,
- project_id: model.id,
- owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: model.visibility.downcase
- }
- end
-
- def builder_driven_event_data_available?(model)
- model.class.in?(BUILDER_DRIVEN_EVENT_DATA_AVAILABLE_FOR_CLASSES)
- end
-
- def builder_driven_event_data(model, event)
builder_class = case model
when GroupMember
Gitlab::HookData::GroupMemberBuilder
@@ -84,6 +29,10 @@ class SystemHooksService
Gitlab::HookData::ProjectMemberBuilder
when User
Gitlab::HookData::UserBuilder
+ when Project
+ Gitlab::HookData::ProjectBuilder
+ when Key
+ Gitlab::HookData::KeyBuilder
end
builder_class.new(model).build(event)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 4377bd8554b..56a6244eebf 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -338,4 +338,4 @@ module SystemNoteService
end
end
-SystemNoteService.prepend_if_ee('EE::SystemNoteService')
+SystemNoteService.prepend_mod_with('SystemNoteService')
diff --git a/app/services/system_notes/base_service.rb b/app/services/system_notes/base_service.rb
index 7341a25b133..ee7784c127b 100644
--- a/app/services/system_notes/base_service.rb
+++ b/app/services/system_notes/base_service.rb
@@ -13,10 +13,10 @@ module SystemNotes
protected
def create_note(note_summary)
- note = Note.create(note_summary.note.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+ note_params = note_summary.note.merge(system: true)
+ note_params[:system_note_metadata] = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
- note
+ Note.create(note_params)
end
def content_tag(*args)
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 60dd56e772a..ae4f65e785c 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -178,8 +178,7 @@ module SystemNotes
if noteable.is_a?(ExternalIssue)
noteable.project.external_issue_tracker.create_cross_reference_note(noteable, mentioner, author)
else
- issue_activity_counter.track_issue_cross_referenced_action(author: author) if noteable.is_a?(Issue)
-
+ track_cross_reference_action
create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
end
end
@@ -414,7 +413,11 @@ module SystemNotes
def issue_activity_counter
Gitlab::UsageDataCounters::IssueActivityUniqueCounter
end
+
+ def track_cross_reference_action
+ issue_activity_counter.track_issue_cross_referenced_action(author: author) if noteable.is_a?(Issue)
+ end
end
end
-SystemNotes::IssuablesService.prepend_if_ee('::EE::SystemNotes::IssuablesService')
+SystemNotes::IssuablesService.prepend_mod_with('SystemNotes::IssuablesService')
diff --git a/app/services/system_notes/time_tracking_service.rb b/app/services/system_notes/time_tracking_service.rb
index 650e40680b1..a804a06fe4c 100644
--- a/app/services/system_notes/time_tracking_service.rb
+++ b/app/services/system_notes/time_tracking_service.rb
@@ -62,12 +62,12 @@ module SystemNotes
if time_spent == :reset
body = "removed time spent"
else
- spent_at = noteable.spent_at
+ spent_at = noteable.spent_at&.to_date
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
text_parts = ["#{action} #{parsed_time} of time spent"]
- text_parts << "at #{spent_at}" if spent_at
+ text_parts << "at #{spent_at}" if spent_at && spent_at != DateTime.current.to_date
body = text_parts.join(' ')
end
diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb
index db47bc024ba..e9a13cee764 100644
--- a/app/services/terraform/remote_state_handler.rb
+++ b/app/services/terraform/remote_state_handler.rb
@@ -94,7 +94,7 @@ module Terraform
end
def find_state!(find_params)
- find_state(find_params) || raise(ActiveRecord::RecordNotFound.new("Couldn't find state"))
+ find_state(find_params) || raise(ActiveRecord::RecordNotFound, "Couldn't find state")
end
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e473a6dc594..fc6543a8efc 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -43,11 +43,11 @@ class TodoService
# updates the todo counts for those users.
#
def destroy_target(target)
- todo_users = UsersWithPendingTodosFinder.new(target).execute.to_a
+ todo_user_ids = target.todos.distinct_user_ids
yield target
- Users::UpdateTodoCountCacheService.new(todo_users).execute if todo_users.present?
+ Users::UpdateTodoCountCacheService.new(todo_user_ids).execute if todo_user_ids.present?
end
# When we reassign an assignable object (issuable, alert) we should:
@@ -224,7 +224,7 @@ class TodoService
return if users.empty?
- users_with_pending_todos = pending_todos(users, attributes).pluck_user_id
+ users_with_pending_todos = pending_todos(users, attributes).distinct_user_ids
users.reject! { |user| users_with_pending_todos.include?(user.id) && Feature.disabled?(:multiple_todos, user) }
todos = users.map do |user|
@@ -234,7 +234,7 @@ class TodoService
Todo.create(attributes.merge(user_id: user.id))
end
- Users::UpdateTodoCountCacheService.new(users).execute
+ Users::UpdateTodoCountCacheService.new(users.map(&:id)).execute
todos
end
@@ -371,4 +371,4 @@ class TodoService
end
end
-TodoService.prepend_if_ee('EE::TodoService')
+TodoService.prepend_mod_with('TodoService')
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
index 6cdd8c16894..fadc76b1181 100644
--- a/app/services/todos/destroy/confidential_issue_service.rb
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -37,7 +37,7 @@ module Todos
def todos
Todo.joins_issue_and_assignees
.where(target: issues)
- .where('issues.confidential = ?', true)
+ .where(issues: { confidential: true })
.where('todos.user_id != issues.author_id')
.where('todos.user_id != issue_assignees.user_id')
end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index 6d4fc3865ac..dfe14225ade 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -9,7 +9,7 @@ module Todos
def initialize(user_id, entity_id, entity_type)
unless %w(Group Project).include?(entity_type)
- raise ArgumentError.new("#{entity_type} is not an entity user can leave")
+ raise ArgumentError, "#{entity_type} is not an entity user can leave"
end
@user = UserFinder.new(user_id).find_by_id
@@ -143,4 +143,4 @@ module Todos
end
end
-Todos::Destroy::EntityLeaveService.prepend_if_ee('EE::Todos::Destroy::EntityLeaveService')
+Todos::Destroy::EntityLeaveService.prepend_mod_with('Todos::Destroy::EntityLeaveService')
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 11727f05f35..80490bd4c9a 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -26,4 +26,4 @@ class UserProjectAccessChangedService
end
end
-UserProjectAccessChangedService.prepend_if_ee('EE::UserProjectAccessChangedService')
+UserProjectAccessChangedService.prepend_mod_with('UserProjectAccessChangedService')
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
index 64844a3f002..c89a286cc8b 100644
--- a/app/services/users/activity_service.rb
+++ b/app/services/users/activity_service.rb
@@ -38,4 +38,4 @@ module Users
end
end
-Users::ActivityService.prepend_ee_mod
+Users::ActivityService.prepend_mod
diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb
index fea7fc55d90..15486ddcd43 100644
--- a/app/services/users/approve_service.rb
+++ b/app/services/users/approve_service.rb
@@ -47,4 +47,4 @@ module Users
end
end
-Users::ApproveService.prepend_if_ee('EE::Users::ApproveService')
+Users::ApproveService.prepend_mod_with('Users::ApproveService')
diff --git a/app/services/users/ban_service.rb b/app/services/users/ban_service.rb
new file mode 100644
index 00000000000..247ed14966b
--- /dev/null
+++ b/app/services/users/ban_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Users
+ class BanService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ if user.ban
+ log_event(user)
+ success
+ else
+ messages = user.errors.full_messages
+ error(messages.uniq.join('. '))
+ end
+ end
+
+ private
+
+ def log_event(user)
+ Gitlab::AppLogger.info(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ end
+ end
+end
diff --git a/app/services/users/block_service.rb b/app/services/users/block_service.rb
index 8513664ee85..37921c477b4 100644
--- a/app/services/users/block_service.rb
+++ b/app/services/users/block_service.rb
@@ -26,4 +26,4 @@ module Users
end
end
-Users::BlockService.prepend_if_ee('EE::Users::BlockService')
+Users::BlockService.prepend_mod_with('Users::BlockService')
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index b3b172f9df2..649cf281ab0 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -14,9 +14,11 @@ module Users
end
def execute(skip_authorization: false)
+ @skip_authorization = skip_authorization
+
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_create_user?
- user_params = build_user_params(skip_authorization: skip_authorization)
+ user_params = build_user_params
user = User.new(user_params)
if current_user&.admin?
@@ -37,6 +39,8 @@ module Users
private
+ attr_reader :skip_authorization
+
def identity_attributes
[:extern_uid, :provider]
end
@@ -102,7 +106,7 @@ module Users
]
end
- def build_user_params(skip_authorization:)
+ def build_user_params
if current_user&.admin?
user_params = params.slice(*admin_create_params)
@@ -111,10 +115,10 @@ module Users
end
else
allowed_signup_params = signup_params
- allowed_signup_params << :skip_confirmation if skip_authorization
+ allowed_signup_params << :skip_confirmation if allow_caller_to_request_skip_confirmation?
user_params = params.slice(*allowed_signup_params)
- if user_params[:skip_confirmation].nil?
+ if assign_skip_confirmation_from_settings?(user_params)
user_params[:skip_confirmation] = skip_user_confirmation_email_from_setting
end
@@ -136,6 +140,14 @@ module Users
user_params
end
+ def allow_caller_to_request_skip_confirmation?
+ skip_authorization
+ end
+
+ def assign_skip_confirmation_from_settings?(user_params)
+ user_params[:skip_confirmation].nil?
+ end
+
def skip_user_confirmation_email_from_setting
!Gitlab::CurrentSettings.send_user_confirmation_email
end
@@ -150,4 +162,4 @@ module Users
end
end
-Users::BuildService.prepend_if_ee('EE::Users::BuildService')
+Users::BuildService.prepend_mod_with('Users::BuildService')
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index ec8b3cea664..757ebd783ee 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -26,4 +26,4 @@ module Users
end
end
-Users::CreateService.prepend_if_ee('EE::Users::CreateService')
+Users::CreateService.prepend_mod_with('Users::CreateService')
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 613d2e4ad82..4ec875098fa 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -31,7 +31,7 @@ module Users
end
if !delete_solo_owned_groups && user.solo_owned_groups.present?
- user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
+ user.errors.add(:base, 'You must transfer ownership or delete groups before you can remove user')
return user
end
@@ -73,4 +73,4 @@ module Users
end
end
-Users::DestroyService.prepend_if_ee('EE::Users::DestroyService')
+Users::DestroyService.prepend_mod_with('Users::DestroyService')
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 1b46edd4d7d..a471f55e644 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -93,4 +93,4 @@ module Users
end
end
-Users::MigrateToGhostUserService.prepend_if_ee('EE::Users::MigrateToGhostUserService')
+Users::MigrateToGhostUserService.prepend_mod_with('Users::MigrateToGhostUserService')
diff --git a/app/services/users/registrations_build_service.rb b/app/services/users/registrations_build_service.rb
new file mode 100644
index 00000000000..9d7bf0a7e18
--- /dev/null
+++ b/app/services/users/registrations_build_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Users
+ class RegistrationsBuildService < BuildService
+ extend ::Gitlab::Utils::Override
+
+ private
+
+ override :allow_caller_to_request_skip_confirmation?
+ def allow_caller_to_request_skip_confirmation?
+ true
+ end
+
+ override :assign_skip_confirmation_from_settings?
+ def assign_skip_confirmation_from_settings?(user_params)
+ user_params[:skip_confirmation].blank?
+ end
+ end
+end
diff --git a/app/services/users/reject_service.rb b/app/services/users/reject_service.rb
index 0e3eb3e5dde..833c30d9427 100644
--- a/app/services/users/reject_service.rb
+++ b/app/services/users/reject_service.rb
@@ -39,4 +39,4 @@ module Users
end
end
-Users::RejectService.prepend_if_ee('EE::Users::RejectService')
+Users::RejectService.prepend_mod_with('Users::RejectService')
diff --git a/app/services/users/update_assigned_open_issue_count_service.rb b/app/services/users/update_assigned_open_issue_count_service.rb
new file mode 100644
index 00000000000..2ed05853b2f
--- /dev/null
+++ b/app/services/users/update_assigned_open_issue_count_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Users
+ # Service class for calculating and caching the number of assigned open issues for a user.
+ class UpdateAssignedOpenIssueCountService
+ attr_accessor :target_user
+
+ def initialize(target_user:)
+ @target_user = target_user
+
+ raise ArgumentError, "Please provide a target user" unless target_user.is_a?(User)
+ end
+
+ def execute
+ value = calculate_count
+ Rails.cache.write(cache_key, value, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD)
+
+ ServiceResponse.success(payload: { count: value })
+ rescue StandardError => e
+ ServiceResponse.error(message: e.message)
+ end
+
+ private
+
+ def cache_key
+ ['users', target_user.id, 'assigned_open_issues_count']
+ end
+
+ def calculate_count
+ IssuesFinder.new(target_user, assignee_id: target_user.id, state: 'opened', non_archived: true).execute.count
+ end
+ end
+end
diff --git a/app/services/users/update_canonical_email_service.rb b/app/services/users/update_canonical_email_service.rb
index e75452f60fd..c4b7a98f60b 100644
--- a/app/services/users/update_canonical_email_service.rb
+++ b/app/services/users/update_canonical_email_service.rb
@@ -7,7 +7,7 @@ module Users
INCLUDED_DOMAINS_PATTERN = [/gmail.com/].freeze
def initialize(user:)
- raise ArgumentError.new("Please provide a user") unless user.is_a?(User)
+ raise ArgumentError, "Please provide a user" unless user.is_a?(User)
@user = user
end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index b69720eefd6..ff08c806319 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -34,7 +34,7 @@ module Users
def execute!(*args, &block)
result = execute(*args, &block)
- raise ActiveRecord::RecordInvalid.new(@user) unless result[:status] == :success
+ raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success
true
end
@@ -96,4 +96,4 @@ module Users
end
end
-Users::UpdateService.prepend_if_ee('EE::Users::UpdateService')
+Users::UpdateService.prepend_mod_with('Users::UpdateService')
diff --git a/app/services/users/update_todo_count_cache_service.rb b/app/services/users/update_todo_count_cache_service.rb
index 03ab66bd64a..3407b22e355 100644
--- a/app/services/users/update_todo_count_cache_service.rb
+++ b/app/services/users/update_todo_count_cache_service.rb
@@ -4,31 +4,34 @@ module Users
class UpdateTodoCountCacheService < BaseService
QUERY_BATCH_SIZE = 10
- attr_reader :users
+ attr_reader :user_ids
- # users - An array of User objects
- def initialize(users)
- @users = users
+ # user_ids - An array of User IDs
+ def initialize(user_ids)
+ @user_ids = user_ids
end
def execute
- users.each_slice(QUERY_BATCH_SIZE) do |users_batch|
- todo_counts = Todo.for_user(users_batch).count_grouped_by_user_id_and_state
+ user_ids.each_slice(QUERY_BATCH_SIZE) do |user_ids_batch|
+ todo_counts = Todo.for_user(user_ids_batch).count_grouped_by_user_id_and_state
- users_batch.each do |user|
- update_count_cache(user, todo_counts, :done)
- update_count_cache(user, todo_counts, :pending)
+ user_ids_batch.each do |user_id|
+ update_count_cache(user_id, todo_counts, :done)
+ update_count_cache(user_id, todo_counts, :pending)
end
end
end
private
- def update_count_cache(user, todo_counts, state)
- count = todo_counts.fetch([user.id, state.to_s], 0)
- expiration_time = user.count_cache_validity_period
+ def update_count_cache(user_id, todo_counts, state)
+ count = todo_counts.fetch([user_id, state.to_s], 0)
- Rails.cache.write(['users', user.id, "todos_#{state}_count"], count, expires_in: expiration_time)
+ Rails.cache.write(
+ ['users', user_id, "todos_#{state}_count"],
+ count,
+ expires_in: User::COUNT_CACHE_VALIDITY_PERIOD
+ )
end
end
end
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
new file mode 100644
index 00000000000..70a96b3ec6b
--- /dev/null
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Users
+ class UpsertCreditCardValidationService < BaseService
+ def initialize(params)
+ @params = params.to_h.with_indifferent_access
+ end
+
+ def execute
+ ::Users::CreditCardValidation.upsert(@params)
+
+ ServiceResponse.success(message: 'CreditCardValidation was set')
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
+ ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
+ ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
+ end
+ end
+end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index a9e219547d7..eab1e91dc89 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -90,7 +90,7 @@ class VerifyPagesDomainService < BaseService
records.any? do |record|
record == domain.keyed_verification_code || record == domain.verification_code
end
- rescue => err
+ rescue StandardError => err
log_error("Failed to check TXT records on #{domain_name} for #{domain.domain}: #{err}")
false
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 5a51b42f9f9..654d9356739 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -6,6 +6,18 @@ class WebHookService
attr_reader :body, :headers, :code
+ def success?
+ false
+ end
+
+ def redirection?
+ false
+ end
+
+ def internal_server_error?
+ true
+ end
+
def initialize
@headers = Gitlab::HTTP::Response::Headers.new({})
@body = ''
@@ -15,6 +27,7 @@ class WebHookService
REQUEST_BODY_SIZE_LIMIT = 25.megabytes
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
+ MAX_FAILURES = 100
attr_accessor :hook, :data, :hook_name, :request_options
@@ -33,6 +46,8 @@ class WebHookService
end
def execute
+ return { status: :error, message: 'Hook disabled' } unless hook.executable?
+
start_time = Gitlab::Metrics::System.monotonic_time
response = if parsed_url.userinfo.blank?
@@ -76,7 +91,11 @@ class WebHookService
end
def async_execute
- WebHookWorker.perform_async(hook.id, data, hook_name)
+ if rate_limited?(hook)
+ log_rate_limit(hook)
+ else
+ WebHookWorker.perform_async(hook.id, data, hook_name)
+ end
end
private
@@ -104,6 +123,8 @@ class WebHookService
end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
+ handle_failure(response, hook)
+
WebHookLog.create(
web_hook: hook,
trigger: trigger,
@@ -118,6 +139,17 @@ class WebHookService
)
end
+ def handle_failure(response, hook)
+ if response.success? || response.redirection?
+ hook.enable!
+ elsif response.internal_server_error?
+ next_backoff = hook.next_backoff
+ hook.update!(disabled_until: next_backoff.from_now, backoff_count: hook.backoff_count + 1)
+ else
+ hook.update!(recent_failures: hook.recent_failures + 1) if hook.recent_failures < MAX_FAILURES
+ end
+ end
+
def build_headers(hook_name)
@headers ||= begin
{
@@ -142,4 +174,34 @@ class WebHookService
response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
end
+
+ def rate_limited?(hook)
+ return false unless Feature.enabled?(:web_hooks_rate_limit, default_enabled: :yaml)
+ return false if rate_limit.nil?
+
+ Gitlab::ApplicationRateLimiter.throttled?(
+ :web_hook_calls,
+ scope: [hook],
+ threshold: rate_limit
+ )
+ end
+
+ def rate_limit
+ @rate_limit ||= hook.rate_limit
+ end
+
+ def log_rate_limit(hook)
+ payload = {
+ message: 'Webhook rate limit exceeded',
+ hook_id: hook.id,
+ hook_type: hook.type,
+ hook_name: hook_name
+ }
+
+ Gitlab::AuthLogger.error(payload)
+
+ # Also log into application log for now, so we can use this information
+ # to determine suitable limits for gitlab.com
+ Gitlab::AppLogger.error(payload)
+ end
end
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index fd234630633..4ec884469eb 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -61,4 +61,4 @@ module WikiPages
end
end
-WikiPages::BaseService.prepend_if_ee('EE::WikiPages::BaseService')
+WikiPages::BaseService.prepend_mod_with('WikiPages::BaseService')
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index f2fc6b37c34..88275f8c417 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -2,6 +2,8 @@
module WikiPages
class UpdateService < WikiPages::BaseService
+ UpdateError = Class.new(StandardError)
+
def execute(page)
# this class is not thread safe!
@old_slug = page.slug
@@ -10,11 +12,15 @@ module WikiPages
execute_hooks(page)
ServiceResponse.success(payload: { page: page })
else
- ServiceResponse.error(
- message: _('Could not update wiki page'),
- payload: { page: page }
- )
+ raise UpdateError, s_('Could not update wiki page')
end
+ rescue UpdateError, WikiPage::PageChangedError, WikiPage::PageRenameError => e
+ page.update_attributes(@params) # rubocop:disable Rails/ActiveRecordAliases
+
+ ServiceResponse.error(
+ message: e.message,
+ payload: { page: page }
+ )
end
def usage_counter_action
diff --git a/app/uploaders/bulk_imports/export_uploader.rb b/app/uploaders/bulk_imports/export_uploader.rb
new file mode 100644
index 00000000000..356e5ce028e
--- /dev/null
+++ b/app/uploaders/bulk_imports/export_uploader.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportUploader < ImportExportUploader
+ EXTENSION_WHITELIST = %w[ndjson.gz].freeze
+ end
+end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 887cb702acf..95bc2680ed6 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -54,7 +54,7 @@ class FileMover
updated_text = to_model.read_attribute(update_field)
.gsub(temp_file_uploader.markdown_link, uploader.markdown_link)
to_model.update_attribute(update_field, updated_text)
- rescue
+ rescue StandardError
revert
false
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index ea71930062c..be5839b7ec5 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -187,7 +187,6 @@ module ObjectStorage
hash[:TempPath] = workhorse_local_upload_path
end
- hash[:FeatureFlagExtractBase] = Feature.enabled?(:workhorse_extract_filename_base, default_enabled: :yaml)
hash[:MaximumSize] = maximum_size if maximum_size.present?
end
end
@@ -452,7 +451,7 @@ module ObjectStorage
def with_exclusive_lease
lease_key = exclusive_lease_key
uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain
- raise ExclusiveLeaseTaken.new(lease_key) unless uuid
+ raise ExclusiveLeaseTaken, lease_key unless uuid
yield uuid
ensure
@@ -484,7 +483,7 @@ module ObjectStorage
end
file
- rescue => e
+ rescue StandardError => e
# in case of failure delete new file
new_file.delete unless new_file.nil?
# revert back to the old file
@@ -509,4 +508,4 @@ module ObjectStorage
end
end
-ObjectStorage::Concern.include_if_ee('::EE::ObjectStorage::Concern')
+ObjectStorage::Concern.include_mod_with('ObjectStorage::Concern')
diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb
index 6a0899be850..89d6343a9a4 100644
--- a/app/validators/branch_filter_validator.rb
+++ b/app/validators/branch_filter_validator.rb
@@ -20,11 +20,11 @@ class BranchFilterValidator < ActiveModel::EachValidator
value_without_wildcards = value.tr('*', 'x')
unless Gitlab::GitRefValidator.validate(value_without_wildcards)
- record.errors[attribute] << "is not a valid branch name"
+ record.errors.add(attribute, "is not a valid branch name")
end
unless value.length <= 4000
- record.errors[attribute] << "is longer than the allowed length of 4000 characters."
+ record.errors.add(attribute, "is longer than the allowed length of 4000 characters.")
end
end
end
diff --git a/app/validators/cron_validator.rb b/app/validators/cron_validator.rb
index 6f42bdb5f9b..91b9cfcccc4 100644
--- a/app/validators/cron_validator.rb
+++ b/app/validators/cron_validator.rb
@@ -10,7 +10,7 @@ class CronValidator < ActiveModel::EachValidator
cron_parser = Gitlab::Ci::CronParser.new(record.public_send(attribute), record.cron_timezone) # rubocop:disable GitlabSecurity/PublicSend
record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_valid?
else
- raise NonWhitelistedAttributeError.new "Non-whitelisted attribute"
+ raise NonWhitelistedAttributeError, "Non-whitelisted attribute"
end
end
end
diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb
index 8dc6265f471..68f03e8a6a3 100644
--- a/app/validators/json_schema_validator.rb
+++ b/app/validators/json_schema_validator.rb
@@ -54,4 +54,4 @@ class JsonSchemaValidator < ActiveModel::EachValidator
end
end
-JsonSchemaValidator.prepend_ee_mod
+JsonSchemaValidator.prepend_mod
diff --git a/app/validators/json_schemas/helm_metadata.json b/app/validators/json_schemas/helm_metadata.json
new file mode 100644
index 00000000000..7ac36e956f3
--- /dev/null
+++ b/app/validators/json_schemas/helm_metadata.json
@@ -0,0 +1,128 @@
+{
+ "description": "Helm metadata",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "home": {
+ "type": "string"
+ },
+ "sources": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "version": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "keywords": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "maintainers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "icon": {
+ "type": "string"
+ },
+ "apiVersion": {
+ "type": "string"
+ },
+ "condition": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "string"
+ },
+ "appVersion": {
+ "type": "string"
+ },
+ "deprecated": {
+ "type": "boolean"
+ },
+ "annotations": {
+ "type": "object",
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ },
+ "additionalProperties": false
+ }
+ },
+ "kubeVersion": {
+ "type": "string"
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "repository": {
+ "type": "string"
+ },
+ "condition": {
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "import-values": {
+ "type": "array",
+ "items": {
+
+ }
+ },
+ "alias": {
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_-]+$"
+ },
+ "additionalProperties": false
+ }
+ }
+ },
+ "type": {
+ "type": "string",
+ "enum": ["application", "library"]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "name",
+ "version",
+ "apiVersion"
+ ]
+} \ No newline at end of file
diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
index dc4880946b2..7c3720dd2e6 100644
--- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
+++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
@@ -161,6 +161,13 @@
"variables": []
},
{
+ "name": "semgrep",
+ "label": "Semgrep",
+ "enabled": true,
+ "description": "Multi-language scanning",
+ "variables": []
+ },
+ {
"name": "sobelow",
"label": "Sobelow",
"enabled" : true,
diff --git a/app/validators/same_project_association_validator.rb b/app/validators/same_project_association_validator.rb
index 2af2a21fa9a..2fcc369b6ef 100644
--- a/app/validators/same_project_association_validator.rb
+++ b/app/validators/same_project_association_validator.rb
@@ -16,6 +16,6 @@ class SameProjectAssociationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if record.project == value&.project
- record.errors[attribute] << 'must associate the same project'
+ record.errors.add(attribute, 'must associate the same project')
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 1aaea1999e5..872a6bef18b 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,4 +1,4 @@
-- parsed_with_gfm = "Content parsed with #{link_to('GitLab Flavored Markdown', help_page_path('user/markdown'), target: '_blank')}.".html_safe
+- parsed_with_gfm = (_("Content parsed with %{link}.") % { link: link_to('GitLab Flavored Markdown', help_page_path('user/markdown'), target: '_blank') }).html_safe
= form_for @appearance, url: admin_appearances_path, html: { class: 'gl-mt-3' } do |f|
= form_errors(@appearance)
@@ -6,22 +6,22 @@
.row
.col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0 Navigation bar
+ %h4.gl-mt-0= _('Navigation bar')
.col-lg-8
.form-group
- = f.label :header_logo, 'Header logo', class: 'col-form-label label-bold pt-0'
+ = f.label :header_logo, _('Header logo'), class: 'col-form-label label-bold pt-0'
%p
- if @appearance.header_logo?
= image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
+ = link_to _('Remove header logo'), header_logos_admin_appearances_path, data: { confirm: _("Header logo will be removed. Are you sure?") }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: "", accept: 'image/*'
.hint
- Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ = _('Maximum file size is 1MB. Pages are optimized for a 28px tall header logo')
%hr
.row
.col-lg-4.profile-settings-sidebar
@@ -29,27 +29,27 @@
.col-lg-8
.form-group
- = f.label :favicon, 'Favicon', class: 'col-form-label label-bold pt-0'
+ = f.label :favicon, _('Favicon'), class: 'col-form-label label-bold pt-0'
%p
- if @appearance.favicon?
= image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
+ = link_to _('Remove favicon'), favicon_admin_appearances_path, data: { confirm: _("Favicon will be removed. Are you sure?") }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
%hr
= f.hidden_field :favicon_cache
= f.file_field :favicon, class: '', accept: 'image/*'
.hint
- Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
+ = _("Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are %{favicon_extension_whitelist}.") % { favicon_extension_whitelist: favicon_extension_whitelist }
%br
- Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
+ = _("Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.")
= render partial: 'admin/appearances/system_header_footer_form', locals: { form: f }
%hr
.row
.col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0 Sign in/Sign up pages
+ %h4.gl-mt-0= _('Sign in/Sign up pages')
.col-lg-8
.form-group
@@ -67,17 +67,17 @@
= image_tag @appearance.logo_path, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo"
+ = link_to _('Remove logo'), logo_admin_appearances_path, data: { confirm: _("Logo will be removed. Are you sure?") }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: "", accept: 'image/*'
.hint
- Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
+ = _('Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.')
%hr
.row
.col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0 New project pages
+ %h4.gl-mt-0= _('New project pages')
.col-lg-8
.form-group
@@ -90,7 +90,7 @@
%hr
.row
.col-lg-4.profile-settings-sidebar
- %h4.gl-mt-0 Profile image guideline
+ %h4.gl-mt-0= _('Profile image guideline')
.col-lg-8
.form-group
@@ -101,13 +101,13 @@
= parsed_with_gfm
.gl-mt-3.gl-mb-3
- = f.submit 'Update appearance settings', class: 'btn gl-button btn-confirm'
+ = f.submit _('Update appearance settings'), class: 'btn gl-button btn-confirm'
- if @appearance.persisted? || @appearance.updated_at
.mt-4
- if @appearance.persisted?
Preview last save:
- = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Sign-in page'), preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('New project page'), new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.float-right
diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml
index f972b3b5cbf..a317611862c 100644
--- a/app/views/admin/appearances/preview_sign_in.html.haml
+++ b/app/views/admin/appearances/preview_sign_in.html.haml
@@ -1,12 +1,12 @@
-= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
+= render 'devise/shared/tab_single', tab_title: _('Sign in preview')
.login-box
%form.gl-show-field-errors
.form-group
= label_tag :login
- = text_field_tag :login, nil, class: "form-control gl-form-input top", title: 'Please provide your username or email address.'
+ = text_field_tag :login, nil, class: "form-control gl-form-input top", title: _('Please provide your username or email address.')
.form-group
= label_tag :password
- = password_field_tag :password, nil, class: "form-control gl-form-input bottom", title: 'This field is required.'
+ = password_field_tag :password, nil, class: "form-control gl-form-input bottom", title: _('This field is required.')
.form-group
- = button_tag "Sign in", class: "btn gl-button btn-confirm"
+ = button_tag _("Sign in"), class: "btn gl-button btn-confirm"
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index f050c0816b1..fab3ce584f0 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -3,9 +3,9 @@
%fieldset
.form-group
- = f.label :abuse_notification_email, 'Abuse reports notification email', class: 'label-bold'
+ = f.label :abuse_notification_email, _('Abuse reports notification email'), class: 'label-bold'
= f.text_field :abuse_notification_email, class: 'form-control gl-form-input'
.form-text.text-muted
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+ = _('Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.')
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml
new file mode 100644
index 00000000000..398064f9730
--- /dev/null
+++ b/app/views/admin/application_settings/_floc.html.haml
@@ -0,0 +1,22 @@
+- expanded = integration_expanded?('floc_')
+
+%section.settings.no-animate#js-floc-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('FloC|Federated Learning of Cohorts')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = s_('FloC|Configure whether you want to participate in FloC.').html_safe
+ = link_to sprite_icon('question-o'), 'https://github.com/WICG/floc', target: '_blank', class: 'has-tooltip', title: _('More information')
+
+ .settings-content
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-floc-settings'), html: { class: 'fieldset-form', id: 'floc-settings' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :floc_enabled, class: 'form-check-input'
+ = f.label :floc_enabled, s_('FloC|Enable FloC (Federated Learning of Cohorts)'), class: 'form-check-label'
+ = f.submit s_('Save changes'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index 72e7cb0b437..b28a53d8bf6 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -3,7 +3,7 @@
%fieldset
.form-group
- = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'label-bold'
+ = f.label :gitaly_timeout_default, _('Default Timeout Period'), class: 'label-bold'
= f.number_field :gitaly_timeout_default, class: 'form-control gl-form-input'
.form-text.text-muted
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
@@ -12,16 +12,16 @@
worker timeout, the remaining time from the worker timeout would be used to avoid having to terminate
the worker.
.form-group
- = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-bold'
+ = f.label :gitaly_timeout_fast, _('Fast Timeout Period'), class: 'label-bold'
= f.number_field :gitaly_timeout_fast, class: 'form-control gl-form-input'
.form-text.text-muted
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
.form-group
- = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'label-bold'
+ = f.label :gitaly_timeout_medium, _('Medium Timeout Period'), class: 'label-bold'
= f.number_field :gitaly_timeout_medium, class: 'form-control gl-form-input'
.form-text.text-muted
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_initial_branch_name.html.haml b/app/views/admin/application_settings/_initial_branch_name.html.haml
index b5c178641df..f881808e51f 100644
--- a/app/views/admin/application_settings/_initial_branch_name.html.haml
+++ b/app/views/admin/application_settings/_initial_branch_name.html.haml
@@ -1,12 +1,12 @@
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
- - fallback_branch_name = '<code>master</code>'
+ - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
- = f.text_field :default_branch_name, placeholder: 'master', class: 'form-control gl-form-input'
+ = f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
index 0ca8493c596..8de65f267d2 100644
--- a/app/views/admin/application_settings/_package_registry.html.haml
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -45,6 +45,9 @@
= f.label :pypi_max_file_size, _('Maximum PyPI package file size in bytes'), class: 'label-bold'
= f.number_field :pypi_max_file_size, class: 'form-control gl-form-input'
.form-group
+ = f.label :terraform_module_max_file_size, _('Maximum Terraform Module package file size in bytes'), class: 'label-bold'
+ = f.number_field :terraform_module_max_file_size, class: 'form-control gl-form-input'
+ .form-group
= f.label :generic_packages_max_file_size, _('Generic package file size in bytes'), class: 'label-bold'
= f.number_field :generic_packages_max_file_size, class: 'form-control gl-form-input'
= f.submit _('Save %{name} size limits').html_safe % { name: plan.name.capitalize }, class: 'btn gl-button btn-confirm'
diff --git a/app/views/admin/application_settings/_package_registry_limits.html.haml b/app/views/admin/application_settings/_package_registry_limits.html.haml
new file mode 100644
index 00000000000..b1dfd04c55e
--- /dev/null
+++ b/app/views/admin/application_settings/_package_registry_limits.html.haml
@@ -0,0 +1,37 @@
+= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-packages-limits-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ %h5
+ = _('Unauthenticated API request rate limit')
+ .form-group
+ .form-check
+ = f.check_box :throttle_unauthenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_packages_api_checkbox' }
+ = f.label :throttle_unauthenticated_packages_api_enabled, class: 'form-check-label label-bold' do
+ = _('Enable unauthenticated 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_unauthenticated_packages_api_requests_per_period, 'Max unauthenticated API requests per period per IP', class: 'label-bold'
+ = f.number_field :throttle_unauthenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :throttle_unauthenticated_packages_api_period_in_seconds, 'Unauthenticated API rate limit period in seconds', class: 'label-bold'
+ = f.number_field :throttle_unauthenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
+ %hr
+ %h5
+ = _('Authenticated API request rate limit')
+ .form-group
+ .form-check
+ = f.check_box :throttle_authenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_packages_api_checkbox' }
+ = f.label :throttle_authenticated_packages_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_packages_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold'
+ = f.number_field :throttle_authenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :throttle_authenticated_packages_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold'
+ = f.number_field :throttle_authenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
+
+ = f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index d57ae94b084..632aeec6ce3 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -17,7 +17,7 @@
= f.check_box :plantuml_enabled, class: 'form-check-input'
= f.label :plantuml_enabled, _('Enable PlantUML'), class: 'form-check-label'
.form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'label-bold'
+ = f.label :plantuml_url, _('PlantUML URL'), class: 'label-bold'
= f.text_field :plantuml_url, class: 'form-control gl-form-input', placeholder: 'http://your-plantuml-instance:8080'
.form-text.text-muted
Allow rendering of
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 468c1786d6f..f102b3d580b 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -3,31 +3,25 @@
%fieldset
%p
- Enable a Prometheus metrics endpoint at
- %code= metrics_path
- to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available
- = link_to 'here', admin_health_check_path
- \. This setting requires a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
+ - link_to_restart = link_to(_('restart'), help_page_path('administration/restart_gitlab'))
+ = _('Enable a Prometheus metrics endpoint at %{metrics_path} to expose a variety of statistics on the health and performance of GitLab. Additional information on authenticating and connecting to the metrics endpoint is available %{link}.').html_safe % { metrics_path: "<code>#{metrics_path}</code>".html_safe, link: link_to(_('here'), admin_health_check_path) }
+ = _('This setting requires a %{link_to_restart} to take effect.').html_safe % { link_to_restart: link_to_restart }
= link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/index')
.form-group
.form-check
= f.check_box :prometheus_metrics_enabled, class: 'form-check-input'
= f.label :prometheus_metrics_enabled, class: 'form-check-label' do
- Enable Prometheus Metrics
+ = _("Enable Prometheus Metrics")
- unless Gitlab::Metrics.metrics_folder_present?
.form-text.text-muted
- %strong.cred WARNING:
- Environment variable
- %code prometheus_multiproc_dir
- does not exist or is not pointing to a valid directory.
+ %strong.cred= _("WARNING:")
+ = _("Environment variable %{code_start}%{environment_variable}%{code_end} does not exist or is not pointing to a valid directory.").html_safe % { environment_variable: prometheus_multiproc_dir, code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
= link_to sprite_icon('question-o'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
.form-group
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-bold'
+ = f.label :metrics_method_call_threshold, _('Method Call Threshold (ms)'), class: 'label-bold'
= f.number_field :metrics_method_call_threshold, class: 'form-control gl-form-input'
.form-text.text-muted
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index edf6853a1aa..31576d54a04 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -17,7 +17,7 @@
= _("If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.")
- clear_repository_checks_link = _('Clear all repository checks')
- clear_repository_checks_message = _('This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?')
- = link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "gl-button btn btn-sm btn-danger"
+ = link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "gl-button btn btn-sm btn-danger gl-mt-3"
.sub-section
%h4= _("Housekeeping")
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 62d6c973efe..12a9f949750 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -6,14 +6,14 @@
.form-check
= f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
= f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
- Password authentication enabled for web interface
+ = _('Password authentication enabled for web interface')
.form-text.text-muted
- When disabled, an external authentication provider must be used.
+ = _('When disabled, an external authentication provider must be used.')
.form-group
.form-check
= f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
= f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
- Password authentication enabled for Git over HTTP(S)
+ = _('Password authentication enabled for Git over HTTP(S)')
.form-text.text-muted
When disabled, a Personal Access Token
- if Gitlab::Auth::Ldap::Config.enabled?
@@ -26,11 +26,11 @@
- oauth_providers_checkboxes.each do |source|
= source
.form-group
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'label-bold'
+ = f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
.form-check
= f.check_box :require_two_factor_authentication, class: 'form-check-input'
= f.label :require_two_factor_authentication, class: 'form-check-label' do
- Require all users to set up Two-factor authentication
+ = _('Require all users to set up two-factor authentication')
.form-group
= f.label :admin_mode, _('Admin Mode'), class: 'label-bold'
= sprite_icon('lock', css_class: 'gl-icon')
@@ -50,19 +50,19 @@
'https://docs.gitlab.com/ee/user/profile/unknown_sign_in_notification.html',
target: '_blank'
.form-group
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-bold'
+ = f.label :two_factor_authentication, _('Two-factor grace period (hours)'), class: 'label-bold'
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control gl-form-input', placeholder: '0'
- .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-text.text-muted= _('Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication')
.form-group
- = f.label :home_page_url, 'Home page URL', class: 'label-bold'
+ = f.label :home_page_url, _('Home page URL'), class: 'label-bold'
= f.text_field :home_page_url, class: 'form-control gl-form-input', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
+ %span.form-text.text-muted#home_help_block= _("We will redirect non-logged in users to this page")
.form-group
= f.label :after_sign_out_path, _('After sign-out path'), class: 'label-bold'
= f.text_field :after_sign_out_path, class: 'form-control gl-form-input', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ %span.form-text.text-muted#after_sign_out_path_help_block= _("We will redirect users to this page after they sign out")
.form-group
= f.label :sign_in_text, _('Sign-in text'), class: 'label-bold'
= f.text_area :sign_in_text, class: 'form-control gl-form-input', rows: 4
.form-text.text-muted Markdown enabled
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index 2086fbc9d32..011bce3ca99 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -6,25 +6,25 @@
.form-check
= f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do
- Enable reCAPTCHA
+ = _("Enable reCAPTCHA")
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from creating accounts.')
.form-group
.form-check
= f.check_box :login_recaptcha_protection_enabled, class: 'form-check-input'
= f.label :login_recaptcha_protection_enabled, class: 'form-check-label' do
- Enable reCAPTCHA for login
+ = _("Enable reCAPTCHA for login")
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from brute-force attacks.')
.form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
+ = f.label :recaptcha_site_key, _('reCAPTCHA Site Key'), class: 'label-bold'
= f.text_field :recaptcha_site_key, class: 'form-control gl-form-input'
.form-text.text-muted
- Generate site and private keys at
+ = _("Generate site and private keys at")
%a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold'
+ = f.label :recaptcha_private_key, _('reCAPTCHA Private Key'), class: 'label-bold'
.form-group
= f.text_field :recaptcha_private_key, class: 'form-control gl-form-input'
@@ -41,10 +41,10 @@
= f.check_box :akismet_enabled, class: 'form-check-input'
= f.label :akismet_enabled, class: 'form-check-label' do
Enable Akismet
- %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues
+ %span.form-text.text-muted#akismet_help_block= _("Helps prevent bots from creating issues")
.form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'label-bold'
+ = f.label :akismet_api_key, _('Akismet API Key'), class: 'label-bold'
= f.text_field :akismet_api_key, class: 'form-control gl-form-input'
.form-text.text-muted
Generate API key at
@@ -54,21 +54,21 @@
.form-check
= f.check_box :unique_ips_limit_enabled, class: 'form-check-input'
= f.label :unique_ips_limit_enabled, class: 'form-check-label' do
- Limit sign in from multiple ips
+ = _("Limit sign in from multiple ips")
%span.form-text.text-muted#unique_ip_help_block
- Helps prevent malicious users hide their activity
+ = _("Helps prevent malicious users hide their activity")
.form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'label-bold'
+ = f.label :unique_ips_limit_per_user, _('IPs per user'), class: 'label-bold'
= f.number_field :unique_ips_limit_per_user, class: 'form-control gl-form-input'
.form-text.text-muted
- Maximum number of unique IPs per user
+ = _("Maximum number of unique IPs per user")
.form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'label-bold'
+ = f.label :unique_ips_limit_time_window, _('IP expiration time'), class: 'label-bold'
= f.number_field :unique_ips_limit_time_window, class: 'form-control gl-form-input'
.form-text.text-muted
- How many seconds an IP will be counted towards the limit
+ = _("How many seconds an IP will be counted towards the limit")
.form-group
.form-check
@@ -78,5 +78,9 @@
.form-group
= f.label :spam_check_endpoint_url, _('URL of the external Spam Check endpoint'), class: 'label-bold'
= f.text_field :spam_check_endpoint_url, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :spam_check_api_key, _('Spam Check API Key'), class: 'gl-font-weight-bold'
+ = f.text_field :spam_check_api_key, class: 'form-control gl-form-input'
+ .form-text.text-muted= _('The API key used by GitLab for accessing the Spam Check service endpoint')
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index 482466c4b3b..d6e31a24cf6 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -3,9 +3,8 @@
%fieldset
.form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'label-bold'
+ = f.label :terminal_max_session_time, _('Max session time'), class: 'label-bold'
= f.number_field :terminal_max_session_time, class: 'form-control gl-form-input'
.form-text.text-muted
- Maximum time for web terminal websocket connection (in seconds).
- 0 for unlimited.
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = _('Maximum time for web terminal websocket connection (in seconds). 0 for unlimited.')
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index a2d61bd010f..64e8751bf31 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -8,11 +8,10 @@
.form-check
= f.check_box :version_check_enabled, class: 'form-check-input'
= f.label :version_check_enabled, class: 'form-check-label' do
- Enable version check
+ = _("Enable version check")
.form-text.text-muted
- GitLab will inform you if a new version is available.
- = link_to 'Learn more', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'version-check')
- about what information is shared with GitLab Inc.
+ = _("GitLab will inform you if a new version is available.")
+ = _("%{link_start}Learn more%{link_end} about what information is shared with GitLab Inc.").html_safe % { link_start: "<a href='#{help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")}'>".html_safe, link_end: '</a>'.html_safe }
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.form-check
@@ -28,7 +27,7 @@
%p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
%button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
- .spinner.js-spinner.d-none
+ .gl-spinner.js-spinner.gl-display-none.gl-mr-2
.js-text.d-inline= _('Preview payload')
%pre.usage-data.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
@@ -37,4 +36,4 @@
- deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
- = f.submit 'Save changes', class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_whats_new.html.haml b/app/views/admin/application_settings/_whats_new.html.haml
new file mode 100644
index 00000000000..70ba994d21e
--- /dev/null
+++ b/app/views/admin/application_settings/_whats_new.html.haml
@@ -0,0 +1,13 @@
+= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-whats-new-settings'), html: { class: 'fieldset-form whats-new-settings' } do |f|
+ = form_errors(@application_setting)
+
+ - whats_new_variants.keys.each do |variant|
+ .form-check.gl-mb-4
+ = f.radio_button :whats_new_variant, variant, class: 'form-check-input'
+ = f.label :whats_new_variant, value: variant, class: 'form-check-label' do
+ .font-weight-bold
+ = whats_new_variants_label(variant)
+ .option-description
+ = whats_new_variants_description(variant)
+
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 86226a9de2f..217225e6186 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -112,3 +112,4 @@
= render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow'
= render 'admin/application_settings/eks'
+= render 'admin/application_settings/floc'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 72716e76013..72a27e4523f 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -24,6 +24,17 @@
.settings-content
= render 'ip_limits'
+%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } }
+ .settings-header
+ %h4
+ = _('Package Registry Rate Limits')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure specific limits for Packages API requests that supersede the general user and IP rate limits.')
+ .settings-content
+ = render 'package_registry_limits'
+
%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } }
.settings-header
%h4
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index fd5ce890648..17bf9ba84a2 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -13,6 +13,17 @@
.settings-content
= render 'email'
+%section.settings.as-whats-new-page.no-animate#js-whats-new-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _("What's new")
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _("Configure What's new drawer and content.")
+ .settings-content
+ = render 'whats_new'
+
%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index 4365d8937bd..111cc9c5d7c 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -2,17 +2,16 @@
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
-- if Feature.enabled?(:global_default_branch_name, default_enabled: true)
- %section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4
- = _('Default initial branch name')
- %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
- = expanded_by_default? ? _('Collapse') : _('Expand')
- %p
- = _('Set the default name of the initial branch when creating new repositories through the user interface.')
- .settings-content
- = render 'initial_branch_name'
+%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Default initial branch name')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Set the default name of the initial branch when creating new repositories through the user interface.')
+ .settings-content
+ = render 'initial_branch_name'
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 9ba72caa88e..bab9fa02928 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,7 +1,10 @@
- page_title _("Background Jobs")
-%h3.page-title Background Jobs
-%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
+%h3.page-title= _('Background Jobs')
+%p.light
+ - sidekiq_link_url = 'http://sidekiq.org/'
+ - sidekiq_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: sidekiq_link_url }
+ = html_escape(_('GitLab uses %{linkStart}Sidekiq%{linkEnd} to process background jobs')) % { linkStart: sidekiq_link_start, linkEnd: '</a>'.html_safe }
%hr
.card.gl-rounded-0
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 9a4bb9b0a48..fe5759ecdbf 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -4,7 +4,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
- Your message here
+ = _('Your message here')
.d-flex.justify-content-center
.broadcast-message.broadcast-notification-message.preview.js-broadcast-notification-message-preview.mt-2{ class: ('hidden' unless @broadcast_message.notification? ) }
= sprite_icon('bullhorn', css_class: 'vertical-align-text-top')
@@ -12,7 +12,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
- Your message here
+ = _('Your message here')
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
@@ -55,7 +55,7 @@
= _('Allow users to dismiss the broadcast message')
.form-group.row.js-toggle-colors-container.toggle-colors.hide
.col-sm-2.col-form-label
- = f.label :font, "Font Color"
+ = f.label :font, _("Font Color")
.col-sm-10
= f.color_field :font, class: "form-control gl-form-input text-font-color"
.form-group.row
@@ -77,6 +77,6 @@
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
- if @broadcast_message.persisted?
- = f.submit "Update broadcast message", class: "btn gl-button btn-confirm"
+ = f.submit _("Update broadcast message"), class: "btn gl-button btn-confirm"
- else
- = f.submit "Add broadcast message", class: "btn gl-button btn-confirm"
+ = f.submit _("Add broadcast message"), class: "btn gl-button btn-confirm"
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e34808665bb..2dbb804d537 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -44,7 +44,7 @@
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
- = sprite_icon('question', size: 16, css_class: 'gl-text-gray-700')
+ = sprite_icon('question-o', size: 16, css_class: 'gl-text-blue-600')
.gl-mt-3.text-uppercase
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 0eaf7b60b25..b0b12a01aed 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -6,5 +6,5 @@
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit 'Create', class: 'btn gl-button btn-confirm'
+ = f.submit 'Create', class: 'btn gl-button btn-confirm', data: { qa_selector: "add_deploy_key_button" }
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
diff --git a/app/views/admin/dev_ops_report/_card.html.haml b/app/views/admin/dev_ops_report/_card.html.haml
deleted file mode 100644
index dd6e5c0f108..00000000000
--- a/app/views/admin/dev_ops_report/_card.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-.devops-card-wrapper
- .devops-card{ class: "devops-card-#{score_level(card.percentage_score)}" }
- .devops-card-title
- %h3
- = card.title
- .light-text
- = card.description
- .board-card-scores
- .board-card-score
- .board-card-score-value
- = format_score(card.instance_score)
- .board-card-score-name= _('You')
- .board-card-score
- .board-card-score-value
- = format_score(card.leader_score)
- .board-card-score-name= _('Lead')
- .board-card-score-big
- = number_to_percentage(card.percentage_score, precision: 1)
- .board-card-buttons
- - if card.blog
- %a.btn-svg{ href: card.blog }
- = sprite_icon('information-o')
- - if card.docs
- %a.btn-svg{ href: card.docs }
- = sprite_icon('question-o')
diff --git a/app/views/admin/dev_ops_report/_no_data.html.haml b/app/views/admin/dev_ops_report/_no_data.html.haml
deleted file mode 100644
index e540a4e2bce..00000000000
--- a/app/views/admin/dev_ops_report/_no_data.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-.container.devops-empty
- .col-sm-12.justify-content-center.text-center
- = custom_icon('dev_ops_report_no_data')
- %h4= _('Data is still calculating...')
- %p
- = _('It may be several days before you see feature usage data.')
- = link_to _('Our documentation includes an example DevOps Score report.'), help_page_path('user/admin_area/analytics/dev_ops_report'), target: '_blank'
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index 95ef1298d03..dbd0020e382 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -4,29 +4,7 @@
= render 'callout'
- if !usage_ping_enabled
- #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
-- elsif @metric.blank?
- = render 'no_data'
+ #js-devops-usage-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping/index.md') } }
- else
- .devops
- .gl-my-3.gl-text-gray-400{ data: { testid: 'devops-score-note-text' } }
- = s_('DevopsReport|DevOps score metrics are based on usage over the last 30 days. Last updated: %{timestamp}.').html_safe % { timestamp: @metric.created_at.strftime('%Y-%m-%d %H:%M') }
- .devops-header
- %h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
- = number_to_percentage(@metric.average_percentage_score, precision: 1)
- .devops-header-subtitle
- = s_('DevopsReport|DevOps')
- %br
- = s_('DevopsReport|Score')
- = link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
+ #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, devops_report_docs_path: help_page_path('user/admin_area/analytics/dev_ops_report'), no_data_image_path: image_path('dev_ops_report_no_data.svg') } }
- .devops-cards.board-card-container
- - @metric.cards.each do |card|
- = render 'card', card: card
-
- .devops-steps.d-none.d-lg-block
- - @metric.idea_to_production_steps.each_with_index do |step, index|
- .devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
- = custom_icon("i2p_step_#{index + 1}")
- %h4.devops-step-title
- = step.title
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index df7af86e089..bbc65850794 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -15,7 +15,7 @@
= markdown_field(group, :description)
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
- %span.badge.badge-pill
+ %span.badge.badge-muted.badge-pill.gl-badge.sm
= storage_counter(group.storage_size)
= render_if_exists 'admin/namespace_plan_badge', namespace: group, css_class: 'gl-ml-5 gl-mr-0'
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 78f0fd325fb..a289cea0d5a 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -23,8 +23,8 @@
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
= render_if_exists 'admin/health_check/health_check_url'
%hr
-.card
- .card-header
+.gl-card
+ .gl-card-header
Current Status:
- if no_errors
= sprite_icon('check', css_class: 'cgreen')
@@ -32,7 +32,7 @@
- else
= sprite_icon('warning-solid', css_class: 'cred')
#{ s_('HealthCheck|Unhealthy') }
- .card-body
+ .gl-card-body
- if no_errors
#{ s_('HealthCheck|No Health Problems Detected') }
- else
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 61af7535c1e..a7f947f96ea 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -4,7 +4,7 @@
= _('Recent Deliveries')
%p= _('When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.')
.col-lg-9
- - if hook_logs.any?
+ - if hook_logs.present?
%table.table
%thead
%tr
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index a357c3d9d34..16661efce04 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -3,5 +3,5 @@
.label-actions-list
= link_to edit_admin_label_path(label), class: 'btn btn-default gl-button btn-default-tertiary label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
= sprite_icon('pencil')
- = link_to admin_label_path(label), class: 'btn btn-default gl-button btn-default-tertiary hover-red js-remove-row label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do
+ = link_to admin_label_path(label), class: 'btn btn-default gl-button btn-default-tertiary hover-red js-remove-label label-action has-tooltip', title: _('Delete'), data: { placement: 'bottom', confirm: "Delete this label? Are you sure?" }, aria_label: _('Delete'), method: :delete, remote: true do
= sprite_icon('remove')
diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml
deleted file mode 100644
index 5ee53088230..00000000000
--- a/app/views/admin/labels/destroy.js.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if @labels.size == 0
- var emptyState = document.querySelector('.labels .nothing-here-block.hidden');
- if (emptyState) emptyState.classList.remove('hidden');
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 7e505729213..6f7cea85ed1 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -1,7 +1,7 @@
.js-projects-list-holder
- if @projects.any?
%ul.projects-list.content-list.admin-projects
- - @projects.each_with_index do |project|
+ - @projects.each do |project|
%li.project-row{ class: ('no-description' if project.description.blank?) }
.controls
= link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button btn-default"
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index 6c75dfe9733..9d42a2bfa93 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -4,9 +4,7 @@
= page_title
.bs-callout.clearfix
- Pass the header
- %code X-Profile-Token: #{@profile_token}
- to profile the request
+ = html_escape(_('Pass the header %{codeOpen} X-Profile-Token: %{profile_token} %{codeClose} to profile the request')) % { profile_token: @profile_token, codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe }
- if @profiles.present?
.gl-mt-3
@@ -21,4 +19,4 @@
admin_requests_profile_path(profile)
- else
%p
- No profiles found
+ = _('No profiles found')
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a38615d9b1b..359e5b411b1 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -5,7 +5,7 @@
.col-sm-6
.bs-callout
%p
- = (_"Runners are processes that pick up and execute CI/CD jobs for GitLab.")
+ = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
%br
= _('You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.')
%br
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 705716c09b7..d911f35d946 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -1,11 +1,11 @@
- add_page_specific_style 'page_bundles/ci_status'
-- page_title @runner.short_sha
+- breadcrumb_title @runner.short_sha
+- page_title "##{@runner.id} (#{@runner.short_sha})"
- add_to_breadcrumbs _('Runners'), admin_runners_path
-- breadcrumb_title page_title
- if Feature.enabled?(:runner_detailed_view_vue_ui, current_user, default_enabled: :yaml)
- #js-runner-detail{ data: {runner_id: @runner.id} }
+ #js-runner-details{ data: {runner_id: @runner.id} }
- else
%h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
@@ -46,9 +46,10 @@
%tr
%td
= form_tag admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ .input-group
+ = search_field_tag :search, params[:search], class: 'form-control gl-form-input', spellcheck: false
+ .input-group-append
+ = submit_tag _('Search'), class: 'gl-button btn btn-default'
%td
- @projects.each do |project|
@@ -59,7 +60,7 @@
.float-right
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
= f.hidden_field :runner_id, value: @runner.id
- = f.submit 'Enable', class: 'gl-button btn btn-sm'
+ = f.submit _('Enable'), class: 'gl-button btn btn-sm'
= paginate_without_count @projects
.col-md-6
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 40fbc559d72..2a36c991ed2 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -1,22 +1,22 @@
- page_title _("Spam Logs")
-%h3.page-title Spam Logs
+%h3.page-title= _('Spam Logs')
%hr
- if @spam_logs.present?
.table-holder
%table.table
%thead
%tr
- %th Date
- %th User
- %th Source IP
- %th API?
- %th Recaptcha verified?
- %th Type
- %th Title
- %th Description
- %th Primary Action
+ %th= _('Date')
+ %th= _('User')
+ %th= _('Source IP')
+ %th= _('API?')
+ %th= _('Recaptcha verified?')
+ %th= _('Type')
+ %th= _('Title')
+ %th= _('Description')
+ %th= _('Primary Action')
%th
= render @spam_logs
= paginate @spam_logs, theme: 'gitlab'
- else
- %h4 There are no Spam Logs
+ %h4= _('There are no Spam Logs')
diff --git a/app/views/admin/users/_ban_user.html.haml b/app/views/admin/users/_ban_user.html.haml
new file mode 100644
index 00000000000..229c88adb7f
--- /dev/null
+++ b/app/views/admin/users/_ban_user.html.haml
@@ -0,0 +1,9 @@
+- if ban_feature_available?
+ .card.border-warning
+ .card-header.bg-warning.gl-text-white
+ = s_('AdminUsers|Ban user')
+ .card-body
+ = user_ban_effects
+ %br
+ %button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_ban_data(user) }
+ = s_('AdminUsers|Ban user')
diff --git a/app/views/admin/users/_cohorts.html.haml b/app/views/admin/users/_cohorts.html.haml
index 013c6072165..25b30adc5be 100644
--- a/app/views/admin/users/_cohorts.html.haml
+++ b/app/views/admin/users/_cohorts.html.haml
@@ -1,4 +1 @@
-- if @cohorts
- = render 'cohorts_table'
-- else
- #js-cohorts-empty-state{ data: { empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('user/admin_area/analytics/user_cohorts') } }
+= render 'cohorts_table'
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index b3ed8369263..9d62c19e2fc 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -3,40 +3,38 @@
= form_errors(@user)
%fieldset
- %legend Account
+ %legend= _('Account')
.form-group.row
.col-sm-2.col-form-label
= f.label :name
.col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
- %span.help-inline * required
+ %span.help-inline * #{_('required')}
.form-group.row
.col-sm-2.col-form-label
= f.label :username
.col-sm-10
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input'
- %span.help-inline * required
+ %span.help-inline * #{_('required')}
.form-group.row
.col-sm-2.col-form-label
= f.label :email
.col-sm-10
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
- %span.help-inline * required
+ %span.help-inline * #{_('required')}
- if @user.new_record?
%fieldset
- %legend Password
+ %legend= _('Password')
.form-group.row
.col-sm-2.col-form-label
= f.label :password
.col-sm-10
%strong
- Reset link will be generated and sent to the user.
- %br
- User will be forced to set the password on first sign in.
+ = _('Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in.').html_safe % { break: '<br />'.html_safe }
- else
%fieldset
- %legend Password
+ %legend= _('Password')
.form-group.row
.col-sm-2.col-form-label
= f.label :password
@@ -55,7 +53,7 @@
= render_if_exists 'admin/users/limits', f: f
%fieldset
- %legend Profile
+ %legend= _('Profile')
.form-group.row
.col-sm-2.col-form-label
= f.label :avatar
@@ -87,8 +85,8 @@
.form-actions
- if @user.new_record?
- = f.submit 'Create user', class: "btn gl-button btn-confirm"
- = link_to 'Cancel', admin_users_path, class: "gl-button btn btn-default btn-cancel"
+ = f.submit _('Create user'), class: "btn gl-button btn-confirm"
+ = link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel"
- else
- = f.submit 'Save changes', class: "btn gl-button btn-confirm"
- = link_to 'Cancel', admin_user_path(@user), class: "gl-button btn btn-default btn-cancel"
+ = f.submit _('Save changes'), class: "btn gl-button btn-confirm"
+ = link_to _('Cancel'), admin_user_path(@user), class: "gl-button btn btn-default btn-cancel"
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index ade3581e5b9..be04e87f8b9 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -3,6 +3,9 @@
- if @user.blocked_pending_approval?
%span.cred
= s_('AdminUsers|(Pending approval)')
+ - elsif @user.banned?
+ %span.cred
+ = s_('AdminUsers|(Banned)')
- elsif @user.blocked?
%span.cred
= s_('AdminUsers|(Blocked)')
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 4fcb9aad343..e90dab68b39 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -1,31 +1,31 @@
.card
.card-header
- Profile
+ = _('Profile')
%ul.content-list
%li
- %span.light Member since
+ %span.light= _('Member since')
%strong= user.created_at.to_s(:medium)
- unless user.public_email.blank?
%li
- %span.light E-mail:
+ %span.light= _('E-mail:')
%strong= link_to user.public_email, "mailto:#{user.public_email}"
- unless user.skype.blank?
%li
- %span.light Skype:
+ %span.light= _('Skype:')
%strong= link_to user.skype, "skype:#{user.skype}"
- unless user.linkedin.blank?
%li
- %span.light LinkedIn:
+ %span.light= _('LinkedIn:')
%strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}"
- unless user.twitter.blank?
%li
- %span.light Twitter:
+ %span.light= _('Twitter:')
%strong= link_to user.twitter, "https://twitter.com/#{user.twitter}"
- unless user.website_url.blank?
%li
- %span.light Website:
+ %span.light= _('Website:')
%strong= link_to user.short_website_url, user.full_website_url
- unless user.location.blank?
%li
- %span.light Location:
+ %span.light= _('Location:')
%strong= user.location
diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index 81cfb71af16..a9f5c560b41 100644
--- a/app/views/admin/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
@@ -1,13 +1,13 @@
- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present?
.card.contributed-projects
- .card-header Projects contributed to
+ .card-header= _('Projects contributed to')
= render 'shared/projects/list',
projects: contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: false
- if local_assigns.has_key?(:projects) && projects.present?
.card
- .card-header Personal projects
+ .card-header= _('Personal projects')
= render 'shared/projects/list',
projects: projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: false
diff --git a/app/views/admin/users/_tabs.html.haml b/app/views/admin/users/_tabs.html.haml
new file mode 100644
index 00000000000..1a3239897eb
--- /dev/null
+++ b/app/views/admin/users/_tabs.html.haml
@@ -0,0 +1,7 @@
+%ul.nav-links.nav-tabs.nav.js-users-tabs{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: admin_users_path, class: active_when(current_page?(admin_users_path)), role: 'tab' }
+ = s_('AdminUsers|Users')
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: cohorts_admin_users_path, class: active_when(current_page?(cohorts_admin_users_path)), role: 'tab' }
+ = s_('AdminUsers|Cohorts')
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index f2920579057..2816a1061b9 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -21,13 +21,13 @@
= user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin)
- unless user.internal?
.table-section.section-20.table-button-footer
- .table-action-buttons
- = link_to _('Edit'), edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn gl-button btn-default'
+ .table-action-buttons{ data: { testid: "user-actions-#{user.id}" } }
+ = link_to _('Edit'), edit_admin_user_path(user), class: 'btn gl-button btn-default'
- unless user == current_user
- %button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { testid: "user-action-button-#{user.id}", toggle: 'dropdown' } }
+ %button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { testid: "dropdown-toggle", toggle: 'dropdown' } }
= sprite_icon('settings')
= sprite_icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right{ data: { testid: "user-action-dropdown-#{user.id}" } }
+ %ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
= _('Settings')
%li
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index c79b2e978f2..e4438f38a47 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -7,39 +7,44 @@
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
= s_('AdminUsers|Active')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.active_without_ghosts)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.active_without_ghosts)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
= s_('AdminUsers|Admins')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
= s_('AdminUsers|2FA Enabled')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
= s_('AdminUsers|2FA Disabled')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
= s_('AdminUsers|External')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
= s_('AdminUsers|Blocked')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.blocked)
+ - if ban_feature_available?
+ = nav_link(html_options: { class: active_when(params[:filter] == 'banned') }) do
+ = link_to admin_users_path(filter: "banned") do
+ = s_('AdminUsers|Banned')
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.banned)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do
= link_to admin_users_path(filter: "blocked_pending_approval"), data: { qa_selector: 'pending_approval_tab' } do
= s_('AdminUsers|Pending approval')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked_pending_approval)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.blocked_pending_approval)
= nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
= link_to admin_users_path(filter: "deactivated") do
= s_('AdminUsers|Deactivated')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.deactivated)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.deactivated)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
= s_('AdminUsers|Without projects')
- %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
+ %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.without_projects)
.nav-controls
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
@@ -68,7 +73,7 @@
= link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do
= title
-- if Feature.enabled?(:vue_admin_users)
+- if Feature.enabled?(:vue_admin_users, default_enabled: :yaml)
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
.gl-spinner-container.gl-my-7
%span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } }
@@ -83,6 +88,6 @@
= render partial: 'admin/users/user', collection: @users
-= paginate @users, theme: "gitlab"
+= paginate_collection @users
= render partial: 'admin/users/modals'
diff --git a/app/views/admin/users/cohorts.html.haml b/app/views/admin/users/cohorts.html.haml
new file mode 100644
index 00000000000..3f3d22fa410
--- /dev/null
+++ b/app/views/admin/users/cohorts.html.haml
@@ -0,0 +1,7 @@
+- page_title _("Users")
+
+= render 'tabs'
+
+.tab-content
+ .tab-pane.active
+ = render 'cohorts'
diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml
index 7d10e839cd6..e3ebb691ba9 100644
--- a/app/views/admin/users/edit.html.haml
+++ b/app/views/admin/users/edit.html.haml
@@ -1,5 +1,5 @@
- page_title _("Edit"), @user.name, _("Users")
%h3.page-title
- Edit user: #{@user.name}
+ = _("Edit user: %{user_name}") % { user_name: @user.name }
%hr
= render 'form'
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index f9b631ed6cf..86b777d8458 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,17 +1,7 @@
- page_title _("Users")
-%ul.nav-links.nav-tabs.nav.js-users-tabs{ role: 'tablist' }
- %li.nav-item.js-users-tab-item{ role: 'presentation' }
- %a.nav-link{ href: '#users', class: active_when(params[:tab] != 'cohorts'), data: { toggle: 'tab' }, role: 'tab' }
- = s_('AdminUsers|Users')
- %li.nav-item.js-users-tab-item{ role: 'presentation' }
- %a.nav-link{ href: '#cohorts', class: active_when(params[:tab] == 'cohorts'), data: { toggle: 'tab' }, role: 'tab' }
- = s_('AdminUsers|Cohorts')
+= render 'tabs'
.tab-content
- .tab-pane{ id: 'users', class: ('active' if params[:tab] != 'cohorts') }
+ .tab-pane.active
= render 'users'
- .tab-pane{ id: 'cohorts', class: ('active' if params[:tab] == 'cohorts') }
- = render 'cohorts'
-
-
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 70a497f14ff..3ff726e1945 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -5,7 +5,7 @@
- if @user.groups.any?
.card
- .card-header Group projects
+ .card-header= _('Group projects')
%ul.hover-list
- @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
- group = group_member.group
@@ -24,12 +24,12 @@
- if @personal_projects.present?
= render 'admin/users/projects', projects: @personal_projects
- else
- .nothing-here-block This user has no personal projects.
+ .nothing-here-block= _('This user has no personal projects.')
.col-md-6
.card
- .card-header Joined projects (#{@joined_projects.count})
+ .card-header= _('Joined projects (%{projects_count})') % { projects_count: @joined_projects.count }
%ul.hover-list
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
@@ -41,10 +41,10 @@
- if member
.float-right
- if member.owner?
- %span.light Owner
+ %span.light= _('Owner')
- else
%span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: 'Remove user from project' do
+ = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do
= sprite_icon('close', size: 16, css_class: 'gl-icon')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index c7ec3ab66d7..19cc29668f5 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -12,7 +12,7 @@
%li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
- %span.light Profile page:
+ %span.light= _('Profile page:')
%strong
= link_to user_path(@user) do
= @user.username
@@ -20,25 +20,25 @@
.card
.card-header
- Account:
+ = _('Account:')
%ul.content-list
%li
- %span.light Name:
+ %span.light= _('Name:')
%strong= @user.name
%li
- %span.light Username:
+ %span.light= _('Username:')
%strong
= @user.username
%li
- %span.light Email:
+ %span.light= _('Email:')
%strong
= render partial: 'shared/email_with_badge', locals: { email: mail_to(@user.email), verified: @user.confirmed? }
- @user.emails.each do |email|
%li
- %span.light Secondary email:
+ %span.light= _('Secondary email:')
%strong
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email } }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
= sprite_icon('close', size: 16, css_class: 'gl-icon')
%li
%span.light ID:
@@ -50,65 +50,68 @@
= @user.namespace_id
%li.two-factor-status
- %span.light Two-factor Authentication:
+ %span.light= _('Two-factor Authentication:')
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
- Enabled
- = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: 'Disable Two-factor Authentication'
+ = _('Enabled')
+ = link_to _('Disable'), disable_two_factor_admin_user_path(@user), data: { confirm: _('Are you sure?') }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
- else
- Disabled
+ = _('Disabled')
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
%li
- %span.light External User:
+ %span.light= _('External User:')
%strong
- = @user.external? ? "Yes" : "No"
+ = @user.external? ? _('Yes') : _('No')
+
+ = render_if_exists 'admin/users/provisioned_by', user: @user
+
%li
- %span.light Can create groups:
+ %span.light= _('Can create groups:')
%strong
- = @user.can_create_group ? "Yes" : "No"
+ = @user.can_create_group ? _('Yes') : _('No')
%li
- %span.light Personal projects limit:
+ %span.light= _('Personal projects limit:')
%strong
= @user.projects_limit
%li
- %span.light Member since:
+ %span.light= _('Member since:')
%strong
= @user.created_at.to_s(:medium)
- if @user.confirmed_at
%li
- %span.light Confirmed at:
+ %span.light= _('Confirmed at:')
%strong
= @user.confirmed_at.to_s(:medium)
- else
%li
- %span.light Confirmed:
+ %span.ligh= _('Confirmed:')
%strong.cred
- No
+ = _('No')
%li
- %span.light Current sign-in IP:
+ %span.light= _('Current sign-in IP:')
%strong
= @user.current_sign_in_ip || _('never')
%li
- %span.light Current sign-in at:
+ %span.light= _('Current sign-in at:')
%strong
= @user.current_sign_in_at&.to_s(:medium) || _('never')
%li
- %span.light Last sign-in IP:
+ %span.light= _('Last sign-in IP:')
%strong
= @user.last_sign_in_ip || _('never')
%li
- %span.light Last sign-in at:
+ %span.light= _('Last sign-in at:')
%strong
= @user.last_sign_in_at&.to_s(:medium) || _('never')
%li
- %span.light Sign-in count:
+ %span.light= _('Sign-in count:')
%strong
= @user.sign_in_count
@@ -121,13 +124,13 @@
- if @user.ldap_user?
%li
- %span.light LDAP uid:
+ %span.light= _('LDAP uid:')
%strong
= @user.ldap_identity.extern_uid
- if @user.created_by
%li
- %span.light Created by:
+ %span.light= _('Created by:')
%strong
= link_to @user.created_by.name, [:admin, @user.created_by]
@@ -140,13 +143,13 @@
- if can_force_email_confirmation?(@user)
.gl-card.border-info.gl-mb-5
.gl-card-header.bg-info.text-white
- Confirm user
+ = _('Confirm user')
.gl-card-body
- if @user.unconfirmed_email.present?
- email = " (#{@user.unconfirmed_email})"
- %p This user has an unconfirmed email address#{email}. You may force a confirmation.
+ %p= _('This user has an unconfirmed email address %{email}. You may force a confirmation.') % { email: email }
%br
- = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
+ = link_to _('Confirm user'), confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?'), qa_selector: 'confirm_user_button' }
= render 'admin/users/user_detail_note'
@@ -154,7 +157,7 @@
- if @user.deactivated?
.gl-card.border-info.gl-mb-5
.gl-card-header.bg-info.text-white
- Reactivate this user
+ = _('Reactivate this user')
.gl-card-body
= render partial: 'admin/users/user_activation_effects'
%br
@@ -163,7 +166,7 @@
- elsif @user.can_be_deactivated?
.gl-card.border-warning.gl-mb-5
.gl-card-header.bg-warning.text-white
- Deactivate this user
+ = _('Deactivate this user')
.gl-card-body
= user_deactivation_effects
%br
@@ -173,36 +176,51 @@
- if @user.blocked_pending_approval?
= render 'admin/users/approve_user', user: @user
= render 'admin/users/reject_pending_user', user: @user
+ - elsif @user.banned?
+ .gl-card.border-info.gl-mb-5
+ .gl-card-header.gl-bg-blue-500.gl-text-white
+ = _('This user is banned')
+ .gl-card-body
+ %p= _('A banned user cannot:')
+ %ul
+ %li= _('Log in')
+ %li= _('Access Git repositories')
+ - link_start = '<a href="%{url}" target="_blank">'.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") }
+ = s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ %p
+ %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unban_data(@user) }
+ = s_('AdminUsers|Unban user')
- else
.gl-card.border-info.gl-mb-5
.gl-card-header.gl-bg-blue-500.gl-text-white
- This user is blocked
+ = _('This user is blocked')
.gl-card-body
- %p A blocked user cannot:
+ %p= _('A blocked user cannot:')
%ul
- %li Log in
- %li Access Git repositories
+ %li= _('Log in')
+ %li= _('Access Git repositories')
%br
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unblock_data(@user) }
= s_('AdminUsers|Unblock user')
- elsif !@user.internal?
= render 'admin/users/block_user', user: @user
+ = render 'admin/users/ban_user', user: @user
- if @user.access_locked?
.card.border-info.gl-mb-5
.card-header.bg-info.text-white
- This account has been locked
+ = _('This account has been locked')
.card-body
- %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
+ %p= _('This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.')
%br
- = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' }
+ = link_to _('Unlock user'), unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?') }
- if !@user.blocked_pending_approval?
.gl-card.border-danger.gl-mb-5
.gl-card-header.bg-danger.text-white
= s_('AdminUsers|Delete user')
.gl-card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
- %p Deleting a user has the following effects:
+ %p= _('Deleting a user has the following effects:')
= render 'users/deletion_guidance', user: @user
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete',
@@ -213,13 +231,13 @@
- else
- if @user.solo_owned_groups.present?
%p
- This user is currently an owner in these groups:
+ = _('This user is currently an owner in these groups:')
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
- You must transfer ownership or delete these groups before you can delete this user.
+ = _('You must transfer ownership or delete these groups before you can delete this user.')
- else
%p
- You don't have access to delete this user.
+ = _("You don't have access to delete this user.")
.gl-card.border-danger
.gl-card-header.bg-danger.text-white
@@ -227,13 +245,8 @@
.gl-card-body
- if can?(current_user, :destroy_user, @user)
%p
- This option deletes the user and any contributions that
- would usually be moved to the
- = succeed "." do
- = link_to "system ghost user", help_page_path("user/profile/account/delete_account")
- As well as the user's personal projects, groups owned solely by
- the user, and projects in them, will also be removed. Commits
- to other projects are unaffected.
+ - link_to_ghost_user = link_to(_("system ghost user"), help_page_path("user/profile/account/delete_account"))
+ = _("This option deletes the user and any contributions that would usually be moved to the %{link_to_ghost_user}. As well as the user's personal projects, groups owned solely by the user, and projects in them, will also be removed. Commits to other projects are unaffected.").html_safe % { link_to_ghost_user: link_to_ghost_user }
%br
%button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions',
delete_user_url: admin_user_path(@user, hard_delete: true),
@@ -242,6 +255,6 @@
= s_('AdminUsers|Delete user and contributions')
- else
%p
- You don't have access to delete this user.
+ = _("You don't have access to delete this user.")
= render partial: 'admin/users/modals'
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 4a84745cf98..6d902132c73 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -3,7 +3,7 @@
%p.js-error-reason
.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' }
- %span.spinner.spinner-dark.spinner-sm{ 'aria-label': 'Loading' }
+ %span.gl-spinner.gl-spinner-dark{ 'aria-label': 'Loading' }
%span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...')
.hidden.row.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
diff --git a/app/views/clusters/clusters/_integrations.html.haml b/app/views/clusters/clusters/_integrations.html.haml
index d718e3ecb26..96219fa9de5 100644
--- a/app/views/clusters/clusters/_integrations.html.haml
+++ b/app/views/clusters/clusters/_integrations.html.haml
@@ -1,19 +1,29 @@
.settings.expanded.border-0.m-0
%p
- = s_('ClusterIntegration|Integrations enable you to integrate your cluster as part of your GitLab workflow.')
+ = s_('ClusterIntegration|Integrations allow you to use applications installed in your cluster as part of your GitLab workflow.')
= link_to _('Learn more'), help_page_path('user/clusters/integrations.md'), target: '_blank'
- .settings-content#advanced-settings-section
+ .settings-content#integrations-settings-section
- if can?(current_user, :admin_cluster, @cluster)
.sub-section.form-group
- = form_for @prometheus_integration, url: @cluster.integrations_path, as: :integration, method: :post, html: { class: 'js-cluster-integrations-form' } do |form|
- = form.hidden_field :application_type
- .form-group
+ = form_for @prometheus_integration, as: :integration, namespace: :prometheus, url: @cluster.integrations_path, method: :post, html: { class: 'js-cluster-integrations-form' } do |prometheus_form|
+ = prometheus_form.hidden_field :application_type
+ .form-group.gl-form-group
.gl-form-checkbox.custom-control.custom-checkbox
- = form.check_box :enabled, { class: 'custom-control-input'}
- = form.label :enabled, s_('ClusterIntegration|Enable Prometheus integration'), class: 'custom-control-label'
- .gl-form-group
+ = prometheus_form.check_box :enabled, class: 'custom-control-input'
+ = prometheus_form.label :enabled, s_('ClusterIntegration|Enable Prometheus integration'), class: 'custom-control-label'
.form-text.text-gl-muted
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("user/clusters/integrations", anchor: "prometheus-cluster-integration") }
- - link_end = '</a>'.html_safe
- = html_escape(s_('ClusterIntegration|Before you enable this integration, follow the %{link_start}documented process%{link_end}.')) % { link_start: link_start, link_end: link_end }
- = form.submit _('Save changes'), class: 'btn gl-button btn-success'
+ = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Prometheus for metrics.')
+ = link_to _('More information.'), help_page_path("user/clusters/integrations", anchor: "prometheus-cluster-integration"), target: '_blank'
+ = prometheus_form.submit _('Save changes'), class: 'btn gl-button btn-success'
+
+ .sub-section.form-group
+ = form_for @elastic_stack_integration, as: :integration, namespace: :elastic_stack, url: @cluster.integrations_path, method: :post, html: { class: 'js-cluster-integrations-form' } do |elastic_stack_form|
+ = elastic_stack_form.hidden_field :application_type
+ .form-group.gl-form-group
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = elastic_stack_form.check_box :enabled, class: 'custom-control-input'
+ = elastic_stack_form.label :enabled, s_('ClusterIntegration|Enable Elastic Stack integration'), class: 'custom-control-label'
+ .form-text.text-gl-muted
+ = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Elasticsearch for pod logs.')
+ = link_to _('More information.'), help_page_path("user/clusters/integrations", anchor: "elastic-stack-cluster-integration"), target: '_blank'
+ = elastic_stack_form.submit _('Save changes'), class: 'btn gl-button btn-success'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 01ba7c06154..001ca80dbd6 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -28,13 +28,13 @@
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_help_path: help_page_path('user/clusters/applications.md', anchor: 'determining-the-external-endpoint-automatically'),
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'),
- environments_help_path: help_page_path('ci/environments/index.md', anchor: 'defining-environments'),
+ environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
- cloud_run_help_path: help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'cloud-run-for-anthos'),
+ cloud_run_help_path: help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'),
manage_prometheus_path: manage_prometheus_path,
cluster_id: @cluster.id,
cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index d617ee0e4cc..ec07c636b79 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -6,4 +6,4 @@
.content_list
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml
index 2f9dbf87d95..d5cd4b66e2b 100644
--- a/app/views/dashboard/groups/_groups.html.haml
+++ b/app/views/dashboard/groups/_groups.html.haml
@@ -1,4 +1,4 @@
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center.prepend-top-20
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index f2f8afb636d..e7d8171d276 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,61 +1,61 @@
-%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
- .todo-avatar
- = author_avatar(todo, size: 40)
-
- .todo-item.todo-block.align-self-center{ data: { qa_selector: "todo_item_container" } }
- .todo-title
- - if todo_author_display?(todo)
- = todo_target_state_pill(todo)
-
- %span.title-item.author-name.bold
- - if todo.author
- = link_to_author(todo, self_added: todo.self_added?)
+%li.todo{ class: "todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
+ .gl-display-flex.gl-flex-direction-row
+ .todo-avatar.gl-display-none.gl-sm-display-inline-block
+ = author_avatar(todo, size: 40)
+
+ .todo-item.gl-w-full.gl-align-self-center{ data: { qa_selector: "todo_item_container" } }
+ .todo-title.gl-mb-3.gl-md-mb-0
+ - if todo_author_display?(todo)
+ = todo_target_state_pill(todo)
+
+ %span.title-item.author-name.bold
+ - if todo.author
+ = link_to_author(todo, self_added: todo.self_added?)
+ - else
+ (removed)
+
+ %span.title-item.action-name{ data: { qa_selector: "todo_action_name_content" } }
+ = todo_action_name(todo)
+
+ %span.title-item.todo-label.todo-target-link
+ - if todo.target
+ = todo_target_link(todo)
- else
- (removed)
-
- %span.title-item.action-name{ data: { qa_selector: "todo_action_name_content" } }
- = todo_action_name(todo)
-
- %span.title-item.todo-label.todo-target-link
- - if todo.target
- = todo_target_link(todo)
- - else
- = _("(removed)")
-
- %span.title-item.todo-target-title{ data: { qa_selector: "todo_target_title_content" } }
- = todo_target_title(todo)
-
- %span.title-item.todo-project.todo-label
- at
- = todo_parent_path(todo)
-
- - if todo.self_assigned?
- %span.title-item.action-name
- = todo_self_addressing(todo)
-
- %span.title-item
- &middot;
-
- %span.title-item.todo-timestamp
- #{time_ago_with_tooltip(todo.created_at)}
- = todo_due_date(todo)
-
- - if todo.note.present?
- .todo-body
- .todo-note.break-word
- .md
- = first_line_in_markdown(todo, :body, 150, project: todo.project)
-
- - if todo.pending?
- .todo-actions
- = link_to dashboard_todo_path(todo), method: :delete, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-done-todo', data: { href: dashboard_todo_path(todo) } do
- Done
- %span.spinner.ml-1
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
- Undo
- %span.spinner.ml-1
- - else
- .todo-actions
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
- Add a to do
- %span.spinner.ml-1
+ = _("(removed)")
+
+ %span.title-item.todo-target-title{ data: { qa_selector: "todo_target_title_content" } }
+ = todo_target_title(todo)
+
+ %span.title-item.todo-project.todo-label
+ at
+ = todo_parent_path(todo)
+
+ - if todo.self_assigned?
+ %span.title-item.action-name
+ = todo_self_addressing(todo)
+
+ %span.title-item
+ &middot;
+
+ %span.title-item.todo-timestamp
+ #{time_ago_with_tooltip(todo.created_at)}
+ = todo_due_date(todo)
+
+ - if todo.note.present?
+ .todo-body
+ .todo-note.break-word
+ .md
+ = first_line_in_markdown(todo, :body, 150, project: todo.project)
+
+ .todo-actions.gl-ml-3
+ - if todo.pending?
+ = link_to dashboard_todo_path(todo), method: :delete, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-done-todo', data: { href: dashboard_todo_path(todo) } do
+ Done
+ %span.gl-spinner.ml-1
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
+ Undo
+ %span.gl-spinner.ml-1
+ - else
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button btn btn-default btn-loading d-flex align-items-center js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
+ Add a to do
+ %span.gl-spinner.ml-1
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index a0016417f0c..52e41946ed1 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -15,13 +15,13 @@
= link_to todos_filter_path(state: 'pending') do
%span
To Do
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= number_with_delimiter(todos_done_count)
.nav-controls
@@ -29,41 +29,41 @@
.gl-mr-3
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'gl-button btn btn-default btn-loading align-items-center js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
- %span.spinner.ml-1
+ %span.gl-spinner.ml-1
= link_to bulk_restore_dashboard_todos_path, class: 'gl-button btn btn-default btn-loading align-items-center js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
Undo mark all as done
- %span.spinner.ml-1
+ %span.gl-spinner.ml-1
.todos-filters
.issues-details-filters.row-content-block.second-block
- = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do
- .filter-categories.flex-fill
- .filter-item.inline
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row' do
+ .filter-categories.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row.gl-flex-fill-1.gl-flex-wrap.gl-mx-n2
+ .filter-item.gl-m-2
- if params[:group_id].present?
= hidden_field_tag(:group_id, params[:group_id])
- = dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
+ = dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit gl-xs-w-full!', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit',
placeholder: 'Search groups', data: { default_label: 'Group', display: 'static' } })
- .filter-item.inline
+ .filter-item.gl-m-2
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit gl-xs-w-full!', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: 'Search projects', data: { default_label: 'Project', display: 'static' } })
- .filter-item.inline
+ .filter-item.gl-m-2
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search gl-xs-w-full!', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } })
- .filter-item.inline
+ .filter-item.gl-m-2
- if params[:type].present?
= hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
data: { data: todo_types_options, default_label: 'Type' } })
- .filter-item.inline.actions-filter
+ .filter-item.actions-filter.gl-m-2
- if params[:action_id].present?
= hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit gl-xs-w-full!', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
data: { data: todo_actions_options, default_label: 'Action' } })
- .filter-item.sort-filter
+ .filter-item.sort-filter.gl-mt-3.gl-sm-mt-0.gl-mb-0.gl-sm-mb-0
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-xs-w-full!', 'data-toggle' => 'dropdown' }
%span.light
@@ -81,40 +81,45 @@
= link_to todos_filter_path(sort: sort_value_oldest_created) do
= sort_title_oldest_created
-.todos-list-container.js-todos-all
+.row.js-todos-all
- if @todos.any?
- .js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
+ .col.js-todos-list-container{ data: { qa_selector: "todos_list_container" } }
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
- .js-nothing-here-container.todos-all-done.hidden.svg-content
- = image_tag 'illustrations/todos_all_done.svg'
- %h4.text-center
- You're all done!
+ .js-nothing-here-container.empty-state.hidden
+ .svg-content
+ = image_tag 'illustrations/todos_all_done.svg'
+ .text-content
+ %h4.text-center
+ You're all done!
- elsif current_user.todos.any?
- .todos-all-done
+ .col.todos-all-done.empty-state
.svg-content.svg-250
= image_tag 'illustrations/todos_all_done.svg'
- - if todos_filter_empty?
- %h4.text-center
- = Gitlab.config.gitlab.no_todos_messages.sample
- %p
- Are you looking for things to do? Take a look at
- = succeed "," do
- = link_to "open issues", issues_dashboard_path
- contribute to
- = link_to "a merge request\,", merge_requests_dashboard_path
- or mention someone in a comment to automatically assign them a new to-do item.
- - else
- %h4.text-center
- Nothing is on your to-do list. Nice work!
+ .text-content
+ - if todos_filter_empty?
+ %h4.text-center
+ = Gitlab.config.gitlab.no_todos_messages.sample
+ %p
+ Are you looking for things to do? Take a look at
+ = succeed "," do
+ %strong
+ = link_to "open issues", issues_dashboard_path
+ contribute to
+ %strong
+ = link_to "a merge request\,", merge_requests_dashboard_path
+ or mention someone in a comment to automatically assign them a new to-do item.
+ - else
+ %h4.text-center
+ Nothing is on your to-do list. Nice work!
- else
- .todos-empty
- .todos-empty-hero.svg-content
+ .col.empty-state
+ .svg-content
= image_tag 'illustrations/todos_empty.svg'
- .todos-empty-content.gl-mx-5
- %h4
+ .text-content
+ %h4.text-center
Your To-Do List shows what to work on next
%p
When an issue or merge request is assigned to you, or when you receive a
diff --git a/app/views/devise/confirmations/new.html.haml b/app/views/devise/confirmations/new.html.haml
index 024ccaddaa1..51354618aa4 100644
--- a/app/views/devise/confirmations/new.html.haml
+++ b/app/views/devise/confirmations/new.html.haml
@@ -6,9 +6,9 @@
= render "devise/shared/error_messages", resource: resource
.form-group
= f.label :email
- = f.email_field :email, class: "form-control gl-form-input", required: true, title: 'Please provide a valid email address.', value: nil
+ = f.email_field :email, class: "form-control gl-form-input", required: true, title: _('Please provide a valid email address.'), value: nil
.clearfix
- = f.submit "Resend", class: 'gl-button btn btn-confirm'
+ = f.submit _("Resend"), class: 'gl-button btn btn-confirm'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
index f14d50eaf71..97fdf0249da 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -1,8 +1,8 @@
#content
- = email_default_heading("#{sanitize_name(@resource.user.name)}, confirm your email address now!")
- %p Click the link below to confirm your email address (#{@resource.email})
+ = email_default_heading(_("%{name}, confirm your email address now!") % { name: sanitize_name(@resource.user.name) })
+ %p= _('Click the link below to confirm your email address (%{email})') % { email: @resource.email }
#cta
- = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
+ = link_to _('Confirm your email address'), confirmation_url(@resource, confirmation_token: @token)
%p
- If this email was added in error, you can remove it here:
- = link_to "Emails", profile_emails_url
+ = _('If this email was added in error, you can remove it here:')
+ = link_to _("Emails"), profile_emails_url
diff --git a/app/views/devise/mailer/reset_password_instructions.html.haml b/app/views/devise/mailer/reset_password_instructions.html.haml
index 47e192afa52..717f51b662f 100644
--- a/app/views/devise/mailer/reset_password_instructions.html.haml
+++ b/app/views/devise/mailer/reset_password_instructions.html.haml
@@ -1,10 +1,9 @@
-= email_default_heading("Hello, #{@resource.name}!")
+= email_default_heading(_("Hello, %{name}!") % { name: @resource.name })
%p
- Someone, hopefully you, has requested to reset the password for your
- GitLab account on #{link_to(Gitlab.config.gitlab.url, Gitlab.config.gitlab.url)}.
+ = _('Someone, hopefully you, has requested to reset the password for your GitLab account on %{link_to_gitlab}.').html_safe % { link_to_gitlab: Gitlab.config.gitlab.url }
%p
- If you did not perform this request, you can safely ignore this email.
+ = _('If you did not perform this request, you can safely ignore this email.')
%p
- Otherwise, click the link below to complete the process.
+ = _('Otherwise, click the link below to complete the process.')
#cta
- = link_to('Reset password', edit_password_url(@resource, reset_password_token: @token))
+ = link_to(_('Reset password'), edit_password_url(@resource, reset_password_token: @token))
diff --git a/app/views/devise/mailer/reset_password_instructions.text.erb b/app/views/devise/mailer/reset_password_instructions.text.erb
index 116313ee11c..c8d86fe998e 100644
--- a/app/views/devise/mailer/reset_password_instructions.text.erb
+++ b/app/views/devise/mailer/reset_password_instructions.text.erb
@@ -1,10 +1,9 @@
-Hello, <%= @resource.name %>!
+<%= _("Hello, %{name}!") % { name: @resource.name } %>
-Someone, hopefully you, has requested to reset the password for your GitLab
-account on <%= Gitlab.config.gitlab.url %>
+<%= _("Someone, hopefully you, has requested to reset the password for your GitLab account on %{link_to_gitlab}.") % { link_to_gitlab: Gitlab.config.gitlab.url } %>
-If you did not perform this request, you can safely ignore this email.
+<%= _("If you did not perform this request, you can safely ignore this email.") %>
-Otherwise, click the link below to complete the process:
+<%= _("Otherwise, click the link below to complete the process:") %>
<%= edit_password_url(@resource, reset_password_token: @token) %>
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
index 8d4abbf3500..9b1e2166cee 100644
--- a/app/views/devise/mailer/unlock_instructions.text.erb
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -1,7 +1,5 @@
-Hello, <%= @resource.name %>!
+<%= _('Hello, %{name}!') % { name: @resource.name } %>
-Your GitLab account has been locked due to an excessive amount of unsuccessful
-sign in attempts. Your account will automatically unlock in <%= distance_of_time_in_words(Devise.unlock_in) %>
-or you may click the link below to unlock now.
+<%= _("Your GitLab account has been locked due to an excessive amount of unsuccessful sign in attempts. Your account will automatically unlock in %{duration} or you may click the link below to unlock now.") % { duration: distance_of_time_in_words(Devise.unlock_in) } %>
<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb
index cb934434c28..f0215f5ea42 100644
--- a/app/views/devise/shared/_links.erb
+++ b/app/views/devise/shared/_links.erb
@@ -1,19 +1,19 @@
<%- if controller_name != 'sessions' %>
- <%= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
+ <%= link_to _("Sign in"), new_session_path(:user, redirect_to_referer: 'yes'), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' && allow_signup? %>
- <%= link_to "Sign up", new_registration_path(:user) %><br />
+ <%= link_to _("Sign up"), new_registration_path(:user) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
-<%= link_to "Forgot your password?", new_password_path(:user), class: "btn" %><br />
+<%= link_to _("Forgot your password?"), new_password_path(:user), class: "btn" %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
- <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(:user) %><br />
+ <%= link_to _("Didn't receive confirmation instructions?"), new_confirmation_path(:user) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
- <%= link_to "Didn't receive unlock instructions?", new_unlock_path(:user) %><br />
+ <%= link_to _("Didn't receive unlock instructions?"), new_unlock_path(:user) %><br />
<% end -%>
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 2fc89f18de6..56f74916d8f 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -37,6 +37,6 @@
= recaptcha_tags
.submit-container
= f.submit button_text, class: 'btn gl-button btn-confirm', data: { qa_selector: 'new_user_register_button' }
- = render 'devise/shared/terms_of_service_notice'
+ = render 'devise/shared/terms_of_service_notice', button_text: button_text
- if show_omniauth_providers && omniauth_providers_placement == :bottom
= render 'devise/shared/signup_omniauth_providers'
diff --git a/app/views/devise/shared/_signup_omniauth_providers_top.haml b/app/views/devise/shared/_signup_omniauth_providers_top.haml
index a2cf5165c1f..9a2629443ed 100644
--- a/app/views/devise/shared/_signup_omniauth_providers_top.haml
+++ b/app/views/devise/shared/_signup_omniauth_providers_top.haml
@@ -1,3 +1,3 @@
-= render 'devise/shared/signup_omniauth_provider_list', providers: trial_enabled_button_based_providers
+= render 'devise/shared/signup_omniauth_provider_list', providers: popular_enabled_button_based_providers
.omniauth-divider.d-flex.align-items-center.text-center
= _("or")
diff --git a/app/views/devise/shared/_terms_of_service_notice.html.haml b/app/views/devise/shared/_terms_of_service_notice.html.haml
index 46b043b2831..75d567a03fd 100644
--- a/app/views/devise/shared/_terms_of_service_notice.html.haml
+++ b/app/views/devise/shared/_terms_of_service_notice.html.haml
@@ -1,5 +1,9 @@
-- company_name = Gitlab.com? ? 'GitLab' : ''
+- return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
-- if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
- %p.gl-text-gray-500.gl-mt-5.gl-mb-0
- = html_escape(_("By clicking Register, I agree that I have read and accepted the %{company_name} %{linkStart}Terms of Use and Privacy Policy%{linkEnd}")) % { linkStart: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, linkEnd: '</a>'.html_safe, company_name: company_name }
+%p.gl-text-gray-500.gl-mt-5.gl-mb-0
+ - if Gitlab.dev_env_or_com?
+ = html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
+ link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
+ - else
+ = html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
+ link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 5e93b1d89eb..f0e7a96f69f 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -17,10 +17,8 @@
= _("An application called %{link_to_client} is requesting access to your GitLab account.").html_safe % { link_to_client: link_to_client }
- auth_app_owner = @pre_auth.client.application.owner
- - if auth_app_owner
- - link_to_owner = link_to(auth_app_owner.name, user_path(auth_app_owner))
- = _("This application was created by %{link_to_owner}.").html_safe % { link_to_owner: link_to_owner }
+ = auth_app_owner_text(auth_app_owner)
= _("Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access.")
- if @pre_auth.scopes
%p
diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml
index a3249275d5e..0358fc524d3 100644
--- a/app/views/explore/groups/_groups.html.haml
+++ b/app/views/explore/groups/_groups.html.haml
@@ -1,4 +1,4 @@
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center.prepend-top-20
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index b1a40bfc96b..1695d3b5539 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -6,4 +6,4 @@
.content_list
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/_archived_projects.html.haml b/app/views/groups/_archived_projects.html.haml
index 48e9f630050..959c26acae0 100644
--- a/app/views/groups/_archived_projects.html.haml
+++ b/app/views/groups/_archived_projects.html.haml
@@ -5,4 +5,4 @@
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
.js-groups-list-holder
.loading-container.text-center.prepend-top-20
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index 2c9d9349f14..2b9277c67e9 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -18,7 +18,8 @@
= f.text_field :bulk_import_gitlab_url, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8',
required: true,
title: s_('GroupsNew|Please fill in GitLab source URL.'),
- id: 'import_gitlab_url'
+ id: 'import_gitlab_url',
+ data: { qa_selector: 'import_gitlab_url' }
.form-group.gl-display-flex.gl-flex-direction-column
= f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token'
.gl-font-weight-normal
@@ -27,6 +28,7 @@
= f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
required: true,
title: s_('GroupsNew|Please fill in your personal access token.'),
- id: 'import_gitlab_token'
+ id: 'import_gitlab_token',
+ data: { qa_selector: 'import_gitlab_token' }
.gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5
- = f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm'
+ = f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'connect_instance_button' }
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index ba6dfcb70ff..69ed94e99cc 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -1,4 +1,4 @@
-- if can_invite_members_for_group?(group)
+- if can?(current_user, :admin_group_member, group)
.js-invite-members-modal{ data: { id: group.id,
name: group.name,
is_project: 'false',
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
index 2769b69add3..bfd056ccdd2 100644
--- a/app/views/groups/_shared_projects.html.haml
+++ b/app/views/groups/_shared_projects.html.haml
@@ -5,4 +5,4 @@
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
.js-groups-list-holder
.loading-container.text-center.prepend-top-20
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml
index d9ab828a83b..651d182b9cc 100644
--- a/app/views/groups/_subgroups_and_projects.html.haml
+++ b/app/views/groups/_subgroups_and_projects.html.haml
@@ -5,4 +5,4 @@
%section{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
.js-groups-list-holder{ data: { show_schema_markup: 'true'} }
.loading-container.text-center.prepend-top-20
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/boards/show.html.haml b/app/views/groups/boards/show.html.haml
index 92838fa4b11..dbbf78eed00 100644
--- a/app/views/groups/boards/show.html.haml
+++ b/app/views/groups/boards/show.html.haml
@@ -1 +1,3 @@
+= render 'shared/alerts/positioning_disabled'
+
= render "shared/boards/show", board: @board, group: true
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 106a7832cc7..45488791272 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,27 +1,26 @@
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
-- can_manage_members = can?(current_user, :admin_group_member, @group)
-- show_invited_members = can_manage_members && @invited_members.exists?
-- show_access_requests = can_manage_members && @requesters.exists?
+- show_invited_members = can_manage_members? && @invited_members.load.any?
+- show_access_requests = can_manage_members? && @requesters.load.any?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap
- - if can_manage_members
+ - if can_manage_members?
.gl-w-half.gl-xs-w-full
%h4
= _('Group members')
%p
= html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- - if can_invite_members_for_group?(@group)
+ - if Feature.enabled?(:invite_members_group_modal, @group)
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- - if can_manage_members && !can_invite_members_for_group?(@group)
+ - if can_manage_members? && Feature.disabled?(:invite_members_group_modal, @group)
%hr.gl-mt-4
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
@@ -42,7 +41,7 @@
%span
= _('Members')
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @members.total_count
- - if @group.shared_with_group_links.any?
+ - if @group.shared_with_group_links.present?
%li.nav-item
= link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
%span
@@ -62,23 +61,21 @@
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless invited_active) }
- .js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
+ .js-group-members-list{ data: { members_data: group_members_list_data_json(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) } }
.loading
- .spinner.spinner-md
- = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
- - if @group.shared_with_group_links.any?
+ .gl-spinner.gl-spinner-md
+ - if @group.shared_with_group_links.present?
#tab-groups.tab-pane
- .js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
+ .js-group-group-links-list{ data: { members_data: group_group_links_list_data_json(@group) } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
- .js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
+ .js-group-invited-members-list{ data: { members_data: group_members_list_data_json(@group, @invited_members, { param_name: :invited_members_page, params: { page: nil } }) } }
.loading
- .spinner.spinner-md
- = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
+ .gl-spinner.gl-spinner-md
- if show_access_requests
#tab-access-requests.tab-pane
- .js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
+ .js-group-access-requests-list{ data: { members_data: group_members_list_data_json(@group, @requesters) } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/groups/imports/show.html.haml b/app/views/groups/imports/show.html.haml
index ac8ca8797fe..79cac364016 100644
--- a/app/views/groups/imports/show.html.haml
+++ b/app/views/groups/imports/show.html.haml
@@ -4,7 +4,7 @@
.save-group-loader
.center
%h2
- %i.loading.spinner.spinner-sm
+ %i.loading.gl-spinner
= page_title
%p
= s_('GroupImport|Please wait while we import the group for you. Refresh at will.')
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index ae4b0807fc5..fdd6962eb21 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,4 +1,4 @@
-- @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.feature_available?(:group_bulk_edit)
+- @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.licensed_feature_available?(:group_bulk_edit)
- page_title _("Issues")
- add_page_specific_style 'page_bundles/issues_list'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 15864e18f7c..33f836c2de0 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,4 +1,4 @@
-- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.feature_available?(:group_bulk_edit)
+- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit)
- page_title _("Merge requests")
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index d4d8a7a57ef..259e96901fd 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -12,7 +12,11 @@
= f.label :description, _("Description")
.col-sm-10
= render layout: 'shared/md_preview', locals: { url: group_preview_markdown_path } do
- = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...'), supports_autocomplete: false
+ = render 'shared/zen', f: f, attr: :description,
+ classes: 'note-textarea',
+ qa_selector: 'milestone_description_field',
+ supports_autocomplete: true,
+ placeholder: _('Write milestone description...')
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f
diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml
index 24eb39b8e2f..f222dba1f90 100644
--- a/app/views/groups/milestones/_header_title.html.haml
+++ b/app/views/groups/milestones/_header_title.html.haml
@@ -1,2 +1,2 @@
- breadcrumb_title @milestone.title
-- add_to_breadcrumbs "Milestones", group_milestones_path(@group)
+- add_to_breadcrumbs _("Milestones"), group_milestones_path(@group)
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 2c93b0e4efd..0d4565706d4 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -3,7 +3,7 @@
- page_title _("Milestones"), @milestone.name, _("Milestones")
%h3.page-title
- New Milestone
+ = _("New Milestone")
%hr
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
index 3794c345aa6..a0d7b8acb47 100644
--- a/app/views/groups/runners/edit.html.haml
+++ b/app/views/groups/runners/edit.html.haml
@@ -1,4 +1,7 @@
-- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners')
+- breadcrumb_title _('Edit')
+- page_title _('Edit'), "##{@runner.id} (#{@runner.short_sha})"
+- add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
+- add_to_breadcrumbs "#{@runner.short_sha}", group_runner_path(@group, @runner)
%h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
diff --git a/app/views/groups/runners/show.html.haml b/app/views/groups/runners/show.html.haml
new file mode 100644
index 00000000000..5cf83e8ccfd
--- /dev/null
+++ b/app/views/groups/runners/show.html.haml
@@ -0,0 +1,3 @@
+- add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
+
+= render 'shared/runners/runner_details', runner: @runner
diff --git a/app/views/groups/settings/_lfs.html.haml b/app/views/groups/settings/_lfs.html.haml
index 77c84862316..b16c9faafa4 100644
--- a/app/views/groups/settings/_lfs.html.haml
+++ b/app/views/groups/settings/_lfs.html.haml
@@ -6,10 +6,8 @@
%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
.form-group.gl-mb-3
- .form-check
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input', data: { qa_selector: 'lfs_checkbox' }
- = f.label :lfs_enabled, class: 'form-check-label' do
- %span
- = _('Allow projects within this group to use Git LFS')
- %br/
- %span.text-muted= _('This setting can be overridden in each project.')
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'custom-control-input', data: { qa_selector: 'lfs_checkbox' }
+ = f.label :lfs_enabled, class: 'custom-control-label' do
+ = _('Allow projects within this group to use Git LFS')
+ %p.help-text= _('This setting can be overridden in each project.')
diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml
index fac3df5237f..bd3b3283288 100644
--- a/app/views/groups/settings/_two_factor_auth.html.haml
+++ b/app/views/groups/settings/_two_factor_auth.html.haml
@@ -7,17 +7,17 @@
%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
.form-group
- .form-check
- = f.check_box :require_two_factor_authentication, class: 'form-check-input', data: { qa_selector: 'require_2fa_checkbox' }
- = f.label :require_two_factor_authentication, class: 'form-check-label' do
- %span= _('Require all users in this group to setup two-factor authentication')
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :require_two_factor_authentication, class: 'custom-control-input', data: { qa_selector: 'require_2fa_checkbox' }
+ = f.label :require_two_factor_authentication, class: 'custom-control-label' do
+ = _('Require all users in this group to setup two-factor authentication')
.form-group
= f.label :two_factor_grace_period, _('Time before enforced'), class: 'label-bold'
= f.text_field :two_factor_grace_period, class: 'form-control form-control-sm w-auto'
.form-text.text-muted= _('Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication')
- unless group.has_parent?
.form-group
- .form-check
- = f.check_box :allow_mfa_for_subgroups, class: 'form-check-input', checked: group.namespace_settings&.allow_mfa_for_subgroups
- = f.label :allow_mfa_for_subgroups, class: 'form-check-label' do
+ .gl-form-checkbox.custom-control.custom-checkbox
+ = f.check_box :allow_mfa_for_subgroups, class: 'custom-control-input', checked: group.namespace_settings&.allow_mfa_for_subgroups
+ = f.label :allow_mfa_for_subgroups, class: 'custom-control-label' do
= _('Allow subgroups to set up their own two-factor authentication rules')
diff --git a/app/views/groups/settings/packages_and_registries/index.html.haml b/app/views/groups/settings/packages_and_registries/show.html.haml
index 1a12ad4902b..1a12ad4902b 100644
--- a/app/views/groups/settings/packages_and_registries/index.html.haml
+++ b/app/views/groups/settings/packages_and_registries/show.html.haml
diff --git a/app/views/groups/settings/repository/_initial_branch_name.html.haml b/app/views/groups/settings/repository/_initial_branch_name.html.haml
index efe690a0c2d..23ac7d51e4f 100644
--- a/app/views/groups/settings/repository/_initial_branch_name.html.haml
+++ b/app/views/groups/settings/repository/_initial_branch_name.html.haml
@@ -9,12 +9,12 @@
.settings-content
= form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@group)
- - fallback_branch_name = '<code>master</code>'
+ - fallback_branch_name = "<code>#{Gitlab::DefaultBranch.value(object: @group)}</code>"
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
- = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: 'master', class: 'form-control'
+ = f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: Gitlab::DefaultBranch.value(object: @group), class: 'form-control'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name }).html_safe
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a1557cda071..9f7f0a08df5 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -12,6 +12,7 @@
is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
track_label: 'invite_members_banner',
invite_members_path: group_group_members_path(@group) } }
+ = render 'groups/invite_members_modal', group: @group
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index 70ac532e69f..755c4151115 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -4,7 +4,10 @@
- add_page_specific_style 'page_bundles/build'
- add_page_specific_style 'page_bundles/ide'
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco')
+
#ide.ide-loading{ data: ide_data }
.text-center
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
%h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index 308065da90a..8a3fe1a816c 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -15,14 +15,14 @@
.form-group.row
= label_tag :bitbucket_server_url, 'Bitbucket Server URL', class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :bitbucket_server_url, '', class: 'form-control gl-mr-3', placeholder: _('https://your-bitbucket-server'), size: 40
+ = text_field_tag :bitbucket_server_url, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('https://your-bitbucket-server'), size: 40
.form-group.row
= label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :bitbucket_server_username, '', class: 'form-control gl-mr-3', placeholder: _('username'), size: 40
+ = text_field_tag :bitbucket_server_username, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('username'), size: 40
.form-group.row
= label_tag :personal_access_token, 'Password/Personal Access Token', class: 'col-form-label col-md-2'
.col-md-4
- = password_field_tag :personal_access_token, '', class: 'form-control gl-mr-3', placeholder: _('Personal Access Token'), size: 40
+ = password_field_tag :personal_access_token, '', class: 'form-control gl-form-input gl-mr-3', placeholder: _('Personal Access Token'), size: 40
.form-actions
= submit_tag _('List your Bitbucket Server repositories'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index 917d88af75a..cd90c76ed10 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -2,9 +2,6 @@
- add_page_specific_style 'page_bundles/import'
- breadcrumb_title _('Import groups')
-%h1.gl-my-0.gl-py-4.gl-font-size-h1.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
- = s_('BulkImport|Import groups from GitLab')
-
#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
available_namespaces_path: import_available_namespaces_path(format: :json),
create_bulk_import_path: import_bulk_imports_path(format: :json),
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index c0abac0a633..ab836174024 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -12,14 +12,14 @@
.form-group.row
= label_tag :uri, _('FogBugz URL'), class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control'
+ = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control gl-form-input'
.form-group.row
= label_tag :email, _('FogBugz Email'), class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :email, nil, class: 'form-control'
+ = text_field_tag :email, nil, class: 'form-control gl-form-input'
.form-group.row
= label_tag :password, _('FogBugz Password'), class: 'col-form-label col-md-2'
.col-md-4
- = password_field_tag :password, nil, class: 'form-control'
+ = password_field_tag :password, nil, class: 'form-control gl-form-input'
.form-actions
= submit_tag _('Continue to the next step'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index 285d2fb23a3..27786806d17 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -13,10 +13,10 @@
.form-group.row
= label_tag :gitea_host_url, _('Gitea Host URL'), class: 'col-form-label col-sm-2'
.col-sm-4
- = text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control'
+ = text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control gl-form-input'
.form-group.row
= label_tag :personal_access_token, _('Personal Access Token'), class: 'col-form-label col-sm-2'
.col-sm-4
- = text_field_tag :personal_access_token, nil, class: 'form-control'
+ = text_field_tag :personal_access_token, nil, class: 'form-control gl-form-input'
.form-actions
= submit_tag _('List Your Gitea Repositories'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/import/phabricator/new.html.haml b/app/views/import/phabricator/new.html.haml
index 69483512816..960d3df2c42 100644
--- a/app/views/import/phabricator/new.html.haml
+++ b/app/views/import/phabricator/new.html.haml
@@ -18,10 +18,10 @@
.form-group.row
= label_tag :phabricator_server_url, _('Phabricator Server URL'), class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :phabricator_server_url, params[:phabricator_server_url], class: 'form-control gl-mr-3', placeholder: 'https://your-phabricator-server', size: 40
+ = text_field_tag :phabricator_server_url, params[:phabricator_server_url], class: 'form-control gl-form-input gl-mr-3', placeholder: 'https://your-phabricator-server', size: 40
.form-group.row
= label_tag :api_token, _('API Token'), class: 'col-form-label col-md-2'
.col-md-4
- = password_field_tag :api_token, params[:api_token], class: 'form-control gl-mr-3', placeholder: _('Personal Access Token'), size: 40
+ = password_field_tag :api_token, params[:api_token], class: 'form-control gl-form-input gl-mr-3', placeholder: _('Personal Access Token'), size: 40
.form-actions
= submit_tag _('Import tasks'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index 561c14dc68a..7de8b0ee10f 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -1,7 +1,7 @@
.row
.form-group.project-name.col-sm-12
= label_tag :name, _('Project name'), class: 'label-bold'
- = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control input-lg", autofocus: true, required: true, aria: { required: true }
+ = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }
.form-group.col-12.col-sm-6
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
.form-group
@@ -18,4 +18,4 @@
= hidden_field_tag :namespace_id, current_user.namespace_id
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
- = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", required: true, aria: { required: true }
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 6694ad5968a..b28cd47efcc 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -32,6 +32,8 @@
- if page_canonical_link
%link{ rel: 'canonical', href: page_canonical_link }
+ = yield :prefetch_asset_tags
+
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= render 'layouts/startup_css'
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index 0ef50d1b122..cd1a236b6be 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -1,10 +1,11 @@
-- if ActionController::Base.asset_host
- %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
- %link{ rel: 'preconnect', href: ActionController::Base.asset_host, crossorigin: '' }
-- if user_application_theme == 'gl-dark'
- %link{ { rel: 'preload', href: stylesheet_url('application_dark'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
-- else
- %link{ { rel: 'preload', href: stylesheet_url('application'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
-%link{ { rel: 'preload', href: stylesheet_url("highlight/themes/#{user_color_scheme}"), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
-- if Gitlab::CurrentSettings.snowplow_enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
- %link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
+= cache_if(Feature.enabled?(:cached_loading_hints, current_user), [ActionController::Base.asset_host, user_application_theme, user_color_scheme], expires_in: 1.minute) do
+ - if ActionController::Base.asset_host
+ %link{ rel: 'dns-prefetch', href: ActionController::Base.asset_host }
+ %link{ rel: 'preconnect', href: ActionController::Base.asset_host, crossorigin: '' }
+ - if user_application_theme == 'gl-dark'
+ %link{ { rel: 'preload', href: stylesheet_url('application_dark'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+ - else
+ %link{ { rel: 'preload', href: stylesheet_url('application'), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+ %link{ { rel: 'preload', href: stylesheet_url("highlight/themes/#{user_color_scheme}"), as: 'style' }, ActionController::Base.asset_host ? { crossorigin: 'anonymous' } : {} }
+ - if Gitlab::CurrentSettings.snowplow_enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
+ %link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
diff --git a/app/views/layouts/_page_title.html.haml b/app/views/layouts/_page_title.html.haml
deleted file mode 100644
index 54da5074763..00000000000
--- a/app/views/layouts/_page_title.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- if content_for?(:page-title)
- = yield :page-title
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 0251a8b6d7c..6bb51b01c13 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -3,7 +3,7 @@
%ul
%li.current-user
- if current_user_menu?(:profile)
- = link_to current_user, class: 'gl-line-height-20!', data: { user: current_user.username, testid: 'user-profile-link' } do
+ = link_to current_user, class: 'gl-line-height-20!', data: { user: current_user.username, testid: 'user-profile-link', qa_selector: 'user_profile_link' } do
= render 'layouts/header/current_user_dropdown_item'
- else
.gl-py-3.gl-px-4
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 481e83c9701..ae333cffb84 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -20,7 +20,7 @@
= _('Next')
- if Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
- = render "layouts/nav/combined_menu"
+ = render "layouts/nav/top_nav"
- else
- if current_user
= render "layouts/nav/dashboard"
@@ -92,7 +92,7 @@
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do
%span.gl-sr-only
= s_('Nav|Help')
- = sprite_icon('question')
+ = sprite_icon('question-o')
%span.notification-dot.rounded-circle.gl-absolute
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index 7b49e6f716e..ca90d2e02fa 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -1,3 +1,4 @@
+- new_repo_experiment_text = content_for(:new_repo_experiment)
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square')
@@ -12,9 +13,9 @@
%li.dropdown-bold-header
= _('This group')
- if create_group_project
- %li= link_to _('New project'), new_project_path(namespace_id: @group.id)
+ %li= link_to new_repo_experiment_text, new_project_path(namespace_id: @group.id), data: { track_experiment: 'new_repo', track_event: 'click_link_new_project_group', track_label: 'plus_menu_dropdown' }
- if create_group_subgroup
- %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id)
+ %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id), data: { track_event: 'click_link_new_subgroup', track_label: 'plus_menu_dropdown' }
= render_if_exists 'layouts/header/create_epic_new_dropdown_item'
= render 'layouts/header/group_invite_members_new_dropdown_item'
%li.divider
@@ -29,16 +30,18 @@
%li.dropdown-bold-header
= _('This project')
- if create_project_issue
- %li= link_to _('New issue'), new_project_issue_path(@project)
+ %li= link_to _('New issue'), new_project_issue_path(@project), data: { track_event: 'click_link_new_issue', track_label: 'plus_menu_dropdown' }
- if merge_project
- %li= link_to _('New merge request'), project_new_merge_request_path(merge_project)
+ %li= link_to _('New merge request'), project_new_merge_request_path(merge_project), data: { track_event: 'click_link_new_mr', track_label: 'plus_menu_dropdown' }
+
- if create_project_snippet
- %li= link_to _('New snippet'), new_project_snippet_path(@project)
+ %li= link_to _('New snippet'), new_project_snippet_path(@project), data: { track_event: 'click_link_new_snippet_project', track_label: 'plus_menu_dropdown' }
= render 'layouts/header/project_invite_members_new_dropdown_item'
%li.divider
%li.dropdown-bold-header GitLab
- = content_for :new_repo_experiment
+ - if current_user.can_create_project?
+ %li= link_to new_repo_experiment_text, new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link_new_project', track_label: 'plus_menu_dropdown' }
- if current_user.can_create_group?
- %li= link_to _('New group'), new_group_path
+ %li= link_to _('New group'), new_group_path, data: { track_event: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
- if current_user.can?(:create_snippet)
- %li= link_to _('New snippet'), new_snippet_path, class: 'qa-global-new-snippet-link'
+ %li= link_to _('New snippet'), new_snippet_path, data: { track_event: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown' }, class: 'qa-global-new-snippet-link'
diff --git a/app/views/layouts/header/_new_repo_experiment.html.haml b/app/views/layouts/header/_new_repo_experiment.html.haml
index 73f960844cb..aaa13d593cd 100644
--- a/app/views/layouts/header/_new_repo_experiment.html.haml
+++ b/app/views/layouts/header/_new_repo_experiment.html.haml
@@ -1,7 +1,6 @@
- content_for :new_repo_experiment do
- - if current_user&.can_create_project?
- - experiment(:new_repo, user: current_user) do |e|
- - e.use do
- %li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
- - e.try do
- %li= link_to _('New project/repository'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
+ - experiment(:new_repo, user: current_user) do |e|
+ - e.use do
+ = _('New project')
+ - e.try do
+ = _('New project/repository')
diff --git a/app/views/layouts/header/_whats_new_dropdown_item.html.haml b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
index 9fe98a54aae..377f0f3271d 100644
--- a/app/views/layouts/header/_whats_new_dropdown_item.html.haml
+++ b/app/views/layouts/header/_whats_new_dropdown_item.html.haml
@@ -2,5 +2,5 @@
%li
%button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' }
= _("What's new")
- %span.js-whats-new-notification-count.whats-new-notification-count
+ %span.js-whats-new-notification-count.gl-badge.badge.sm.badge-dark.badge-pill
= whats_new_most_recent_release_items_count
diff --git a/app/views/layouts/nav/_combined_menu.html.haml b/app/views/layouts/nav/_combined_menu.html.haml
deleted file mode 100644
index db5a7012e8f..00000000000
--- a/app/views/layouts/nav/_combined_menu.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%button{ type: 'button', data: { toggle: "dropdown" } }
- = sprite_icon('ellipsis_v')
- = _('Projects')
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 42e3ae7e717..718b2002422 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,5 +1,7 @@
--# WAIT! Before adding more items to the nav bar, please see
--# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
+-# WARNING! This file is slated to be removed along with the `combined_menu`
+-# feature flag. The logic here will be migrated to an upcoming `top_nav_helper`.
+-# Please see [this MR][1] for more context.
+-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }) do
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 7d18cd8978b..5b47eb27b04 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,3 +1,7 @@
+-# WARNING! This file is slated to be removed along with the `combined_menu`
+-# feature flag. The logic here will be migrated to an upcoming `top_nav_helper`.
+-# Please see [this MR][1] for more context.
+-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
%ul.list-unstyled.navbar-sub-nav
- if explore_nav_link?(:projects)
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
diff --git a/app/views/layouts/nav/_top_nav.html.haml b/app/views/layouts/nav/_top_nav.html.haml
new file mode 100644
index 00000000000..50c003f8e13
--- /dev/null
+++ b/app/views/layouts/nav/_top_nav.html.haml
@@ -0,0 +1,7 @@
+- view_model = top_nav_view_model(project: @project, group: @group)
+%ul.list-unstyled.navbar-sub-nav#js-top-nav{ data: { view_model: view_model.to_json } }
+ %li
+ %a.top-nav-toggle{ href: '#', type: 'button', data: { toggle: "dropdown" } }
+ = sprite_icon('dot-grid', css_class: "dropdown-icon")
+ = view_model[:activeTitle]
+ = sprite_icon('chevron-down')
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
index a9d88341a19..036647e2be1 100644
--- a/app/views/layouts/nav/groups_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml
@@ -1,5 +1,9 @@
+-# WARNING! This file is slated to be removed along with the `combined_menu`
+-# feature flag. The logic here will be migrated to an upcoming `top_nav_helper`.
+-# Please see [this MR][1] for more context.
+-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
- group_meta = { id: @group.id, name: @group.name, namespace: @group.full_name, web_url: group_path(@group), avatar_url: @group.avatar_url } if @group&.persisted?
-.frequent-items-dropdown-container
+.frequent-items-dropdown-container.with-deprecated-styles
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/groups#index') do
@@ -9,10 +13,10 @@
= link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
= _('Explore groups')
= nav_link(path: 'groups/new#create-group-pane', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
- = link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link" } do
+ = link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link", qa_selector: 'create_group_link' } do
= _('Create group')
= nav_link(path: 'groups/new#import-group-pane') do
- = link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link" } do
+ = link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link", qa_selector: 'import_group_link' } do
= _('Import group')
.frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index b95a9cdb00f..2517508ba6c 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,5 +1,9 @@
+-# WARNING! This file is slated to be removed along with the `combined_menu`
+-# feature flag. The logic here will be migrated to an upcoming `top_nav_helper`.
+-# Please see [this MR][1] for more context.
+-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
-.frequent-items-dropdown-container
+.frequent-items-dropdown-container.with-deprecated-styles
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index d756867541b..b71866c9138 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -2,9 +2,9 @@
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: _('Admin Overview') do
- .avatar-container.s40.settings-avatar
+ %span.avatar-container.s40.settings-avatar
= sprite_icon('admin', size: 24)
- .sidebar-context-title
+ %span.sidebar-context-title
= _('Admin Area')
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
= nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do
@@ -202,17 +202,18 @@
= render_if_exists 'layouts/nav/sidebar/credentials_link'
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path do
- .nav-icon-container
- = sprite_icon('template')
- %span.nav-item-name
- = _('Service Templates')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :services, html_options: { class: "fly-out-top-item" } ) do
- = link_to admin_application_settings_services_path do
- %strong.fly-out-top-item-name
- = _('Service Templates')
+ - if show_service_templates_nav_link?
+ = nav_link(controller: :services) do
+ = link_to admin_application_settings_services_path do
+ .nav-icon-container
+ = sprite_icon('template')
+ %span.nav-item-name
+ = _('Service Templates')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :services, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_application_settings_services_path do
+ %strong.fly-out-top-item-name
+ = _('Service Templates')
= nav_link(controller: :labels) do
= link_to admin_labels_path do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 41bec996de1..757f95f864a 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,15 +1,14 @@
- issues_count = cached_issuables_count(@group, type: :issues)
- merge_requests_count = group_open_merge_requests_count(@group)
- aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
-- overview_title = @group.subgroup? ? _('Subgroup overview') : _('Group overview')
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@group), 'aria-label': aside_title }
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
- .avatar-container.rect-avatar.s40.group-avatar
+ %span.avatar-container.rect-avatar.s40.group-avatar
= group_icon(@group, class: "avatar s40 avatar-tile")
- .sidebar-context-title
+ %span.sidebar-context-title
= @group.name
%ul.sidebar-top-level-items.qa-group-sidebar
= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
@@ -19,21 +18,23 @@
= nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
= link_to group_path(@group) do
.nav-icon-container
- = sprite_icon('home')
+ - sprite = Feature.enabled?(:sidebar_refactor, current_user) ? 'group' : 'home'
+ = sprite_icon(sprite)
%span.nav-item-name
- = overview_title
+ = group_information_title(@group)
%ul.sidebar-sub-level-items
- = nav_link(path: ['groups#show', 'groups#details', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
= link_to group_path(@group) do
%strong.fly-out-top-item-name
- = overview_title
+ = group_information_title(@group)
%li.divider.fly-out-top-item
- = nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to details_group_path(@group), title: _('Group details') do
- %span
- = _('Details')
+ - if Feature.disabled?(:sidebar_refactor, current_user)
+ = nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do
+ = link_to details_group_path(@group), title: _('Group details') do
+ %span
+ = _('Details')
- if group_sidebar_link?(:activity)
= nav_link(path: 'groups#activity') do
@@ -41,6 +42,19 @@
%span
= _('Activity')
+ - if group_sidebar_link?(:labels) && Feature.enabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
+ = nav_link(path: 'labels#index') do
+ = link_to group_labels_path(@group), title: _('Labels') do
+ %span
+ = _('Labels')
+
+ - if Feature.enabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
+ - if group_sidebar_link?(:group_members)
+ = nav_link(path: 'group_members#index') do
+ = link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do
+ %span
+ = _('Members')
+
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
@@ -53,7 +67,7 @@
%span.badge.badge-pill.count= issues_count
%ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
- = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index', 'iterations#index'], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
= _('Issues')
@@ -71,7 +85,7 @@
%span
= boards_link_text
- - if group_sidebar_link?(:labels)
+ - if group_sidebar_link?(:labels) && Feature.disabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
%span
@@ -124,25 +138,26 @@
- if group_sidebar_link?(:wiki)
= render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
- - if group_sidebar_link?(:group_members)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group) do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name.qa-group-members-item
- = _('Members')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to group_group_members_path(@group) do
- %strong.fly-out-top-item-name
- = _('Members')
+ - if Feature.disabled?(:sidebar_refactor, current_user, default_enabled: :yaml)
+ - if group_sidebar_link?(:group_members)
+ = nav_link(path: 'group_members#index') do
+ = link_to group_group_members_path(@group) do
+ .nav-icon-container
+ = sprite_icon('users')
+ %span.nav-item-name.qa-group-members-item
+ = _('Members')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_group_members_path(@group) do
+ %strong.fly-out-top-item-name
+ = _('Members')
- if group_sidebar_link?(:settings)
= nav_link(path: group_settings_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
- %span.nav-item-name.qa-group-settings-item
+ %span.nav-item-name{ data: { qa_selector: 'group_settings' } }
= _('Settings')
%ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } }
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
@@ -170,7 +185,7 @@
%span
= _('Repository')
- = nav_link(controller: :ci_cd) do
+ = nav_link(controller: [:ci_cd, 'groups/runners']) do
= link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
%span
= _('CI/CD')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index dda5e6b9636..63b97e3133c 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -2,9 +2,9 @@
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: _('Profile Settings') do
- .avatar-container.s40.settings-avatar
+ %span.avatar-container.s40.settings-avatar
= image_tag avatar_icon_for_user(current_user, 40), class: "avatar s40 avatar-tile js-sidebar-user-avatar", alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
- .sidebar-context-title= _('User Settings')
+ %span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 3d0c6baffd5..a06f9f8d6ef 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,3 +1 @@
--# We're migration the project sidebar to a logical model based structure. If you need to update
--# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_project_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref))
diff --git a/app/views/layouts/nav/sidebar/_project_menus.html.haml b/app/views/layouts/nav/sidebar/_project_menus.html.haml
deleted file mode 100644
index ed072c0f6a2..00000000000
--- a/app/views/layouts/nav/sidebar/_project_menus.html.haml
+++ /dev/null
@@ -1,380 +0,0 @@
-- if project_nav_tab? :issues
- = nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
- = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
- .nav-icon-container
- = sprite_icon('issues')
- %span.nav-item-name#js-onboarding-issues-link
- = _('Issues')
- - if @project.issues_enabled?
- %span.badge.badge-pill.count.issue_counter
- = number_with_delimiter(@project.open_issues_count(current_user))
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_issues_path(@project) do
- %strong.fly-out-top-item-name
- = _('Issues')
- - if @project.issues_enabled?
- %span.badge.badge-pill.count.issue_counter.fly-out-badge
- = number_with_delimiter(@project.open_issues_count(current_user))
- %li.divider.fly-out-top-item
- = nav_link(controller: :issues, action: :index) do
- = link_to project_issues_path(@project), title: _('Issues') do
- %span
- = _('List')
-
- = nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
- %span
- = boards_link_text
-
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
- %span
- = _('Labels')
-
- = render 'projects/sidebar/issues_service_desk'
-
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
- %span
- = _('Milestones')
-
- = render_if_exists 'layouts/nav/sidebar/project_iterations_link'
-
-- if project_nav_tab?(:external_issue_tracker)
- - issue_tracker = @project.external_issue_tracker
- - if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
- = render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
- - else
- = nav_link do
- = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
- .nav-icon-container
- = sprite_icon('external-link')
- %span.nav-item-name
- = issue_tracker.title
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: "fly-out-top-item" } ) do
- = link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
- %strong.fly-out-top-item-name
- = issue_tracker.title
-
-- if (project_nav_tab? :labels) && !@project.issues_enabled?
- = nav_link(controller: [:labels]) do
- = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
- .nav-icon-container
- = sprite_icon('label')
- %span.nav-item-name#js-onboarding-labels-link
- = _('Labels')
-
-- if project_nav_tab? :merge_requests
- = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
- = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
- .nav-icon-container
- = sprite_icon('git-merge')
- %span.nav-item-name#js-onboarding-mr-link
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter
- = number_with_delimiter(@project.open_merge_requests_count)
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_merge_requests_path(@project) do
- %strong.fly-out-top-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
- = number_with_delimiter(@project.open_merge_requests_count)
-
-= render_if_exists "layouts/nav/requirements_link", project: @project
-
-- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
- = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
- .nav-icon-container
- = sprite_icon('rocket')
- %span.nav-item-name#js-onboarding-pipelines-link
- = _('CI/CD')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
- = link_to project_pipelines_path(@project) do
- %strong.fly-out-top-item-name
- = _('CI/CD')
- %li.divider.fly-out-top-item
- - if project_nav_tab? :pipelines
- = nav_link(path: ['pipelines#index', 'pipelines#show']) do
- = link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
- %span
- = _('Pipelines')
-
- - if can_view_pipeline_editor?(@project)
- = nav_link(controller: :pipeline_editor, action: :show) do
- = link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
- %span
- = s_('Pipelines|Editor')
-
- - if project_nav_tab? :builds
- = nav_link(controller: :jobs) do
- = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
- %span
- = _('Jobs')
-
- - if Feature.enabled?(:artifacts_management_page, @project)
- = nav_link(controller: :artifacts, action: :index) do
- = link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
- %span
- = _('Artifacts')
-
- - if project_nav_tab?(:pipelines)
- = nav_link(controller: :pipeline_schedules) do
- = link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
- %span
- = _('Schedules')
-
- = render_if_exists "layouts/nav/test_cases_link", project: @project
-
-- if project_nav_tab? :security_and_compliance
- = render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
-
-- if project_nav_tab? :operations
- = nav_link(controller: sidebar_operations_paths) do
- = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
- .nav-icon-container
- = sprite_icon('cloud-gear')
- %span.nav-item-name
- = _('Operations')
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to sidebar_operations_link_path do
- %strong.fly-out-top-item-name
- = _('Operations')
- %li.divider.fly-out-top-item
-
- - if project_nav_tab? :metrics_dashboards
- = nav_link(controller: :metrics_dashboard, action: [:show]) do
- = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
- %span
- = _('Metrics')
-
- - if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
- = nav_link(controller: :logs, action: [:index]) do
- = link_to project_logs_path(@project), title: _('Logs') do
- %span
- = _('Logs')
-
- - if project_nav_tab? :environments
- = render "layouts/nav/sidebar/tracing_link"
-
- - if project_nav_tab?(:error_tracking)
- = nav_link(controller: :error_tracking) do
- = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
- %span
- = _('Error Tracking')
-
- - if project_nav_tab?(:alert_management)
- = nav_link(controller: :alert_management) do
- = link_to project_alert_management_index_path(@project), title: _('Alerts') do
- %span
- = _('Alerts')
-
- - if project_nav_tab?(:incidents)
- = nav_link(controller: :incidents) do
- = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
- %span
- = _('Incidents')
-
- = render_if_exists 'projects/sidebar/oncall_schedules'
-
- - if project_nav_tab? :serverless
- = nav_link(controller: :functions) do
- = link_to project_serverless_functions_path(@project), title: _('Serverless') do
- %span
- = _('Serverless')
-
- - if project_nav_tab? :terraform
- = nav_link(controller: :terraform) do
- = link_to project_terraform_index_path(@project), title: _('Terraform') do
- %span
- = _('Terraform')
-
- - if project_nav_tab? :clusters
- - show_cluster_hint = show_gke_cluster_integration_callout?(@project)
- = nav_link(controller: [:cluster_agents, :clusters]) do
- = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
- %span
- = _('Kubernetes')
- - if show_cluster_hint
- .js-feature-highlight{ disabled: true,
- data: { trigger: 'manual',
- container: 'body',
- placement: 'right',
- highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
- highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
- dismiss_endpoint: user_callouts_path,
- auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
- - if project_nav_tab? :environments
- = nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
- = link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
- %span
- = _('Environments')
-
- - if project_nav_tab? :feature_flags
- = nav_link(controller: :feature_flags) do
- = link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
- %span
- = _('Feature Flags')
-
- - if project_nav_tab?(:product_analytics)
- = nav_link(controller: :product_analytics) do
- = link_to project_product_analytics_path(@project), title: _('Product Analytics') do
- %span
- = _('Product Analytics')
-
-= render_if_exists 'layouts/nav/sidebar/project_packages_link'
-
-- if project_nav_tab? :analytics
- = render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
-
-- if project_nav_tab?(:confluence)
- - confluence_url = project_wikis_confluence_path(@project)
- = nav_link do
- = link_to confluence_url, class: 'shortcuts-confluence' do
- .nav-icon-container
- = image_tag 'confluence.svg', alt: _('Confluence')
- %span.nav-item-name
- = _('Confluence')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: 'fly-out-top-item' } ) do
- = link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
- %strong.fly-out-top-item-name
- = _('Confluence')
-
-- if project_nav_tab? :wiki
- = render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
-
-- if project_nav_tab?(:external_wiki)
- - external_wiki_url = @project.external_wiki.external_wiki_url
- = nav_link do
- = link_to external_wiki_url, class: 'shortcuts-external_wiki' do
- .nav-icon-container
- = sprite_icon('external-link')
- %span.nav-item-name
- = s_('ExternalWikiService|External wiki')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(html_options: { class: "fly-out-top-item" } ) do
- = link_to external_wiki_url do
- %strong.fly-out-top-item-name
- = s_('ExternalWikiService|External wiki')
-
-- if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
- .nav-icon-container
- = sprite_icon('snippet')
- %span.nav-item-name
- = _('Snippets')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_snippets_path(@project) do
- %strong.fly-out-top-item-name
- = _('Snippets')
-
-= nav_link(controller: :project_members) do
- = link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
- .nav-icon-container
- = sprite_icon('users')
- %span.nav-item-name
- = _('Members')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
- = link_to project_project_members_path(@project) do
- %strong.fly-out-top-item-name
- = _('Members')
-
-- if project_nav_tab? :settings
- = nav_link(path: sidebar_settings_paths) do
- = link_to edit_project_path(@project) do
- .nav-icon-container
- = sprite_icon('settings')
- %span.nav-item-name.qa-settings-item#js-onboarding-settings-link
- = _('Settings')
-
- %ul.sidebar-sub-level-items
- - can_edit = can?(current_user, :admin_project, @project)
- - if can_edit
- = nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to edit_project_path(@project) do
- %strong.fly-out-top-item-name
- = _('Settings')
- %li.divider.fly-out-top-item
- = nav_link(path: %w[projects#edit]) do
- = link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
- %span
- = _('General')
- - if can_edit
- = nav_link(controller: [:integrations, :services]) do
- = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
- %span
- = _('Integrations')
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
- %span
- = _('Webhooks')
- - if can?(current_user, :read_resource_access_tokens, @project)
- = nav_link(controller: [:access_tokens]) do
- = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
- %span
- = _('Access Tokens')
- = nav_link(controller: :repository) do
- = link_to project_settings_repository_path(@project), title: _('Repository') do
- %span
- = _('Repository')
- - if !@project.archived? && @project.feature_available?(:builds, current_user)
- = nav_link(controller: :ci_cd) do
- = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
- %span
- = _('CI/CD')
- - if settings_operations_available?
- = nav_link(controller: [:operations]) do
- = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
- = _('Operations')
- - if @project.pages_available?
- = nav_link(controller: :pages) do
- = link_to project_pages_path(@project), title: _('Pages') do
- %span
- = _('Pages')
-
--# Shortcut to Project > Activity
-%li.hidden
- = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
- %span
- = _('Activity')
-
--# Shortcut to Repository > Graph (formerly, Network)
-- if project_nav_tab? :network
- %li.hidden
- = link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
- = _('Graph')
-
--# Shortcut to Issues > New Issue
-- if project_nav_tab?(:issues)
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- = _('Create a new issue')
-
--# Shortcut to Pipelines > Jobs
-- if project_nav_tab? :builds
- %li.hidden
- = link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
- = _('Jobs')
-
--# Shortcut to commits page
-- if project_nav_tab? :commits
- %li.hidden
- = link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
- = _('Commits')
-
--# Shortcut to issue boards
-- if project_nav_tab?(:issues)
- %li.hidden
- = link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
deleted file mode 100644
index b28468a7969..00000000000
--- a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- packages_link = project_nav_tab?(:packages) ? project_packages_path(@project) : project_container_registry_index_path(@project)
-
-- if (project_nav_tab?(:packages) || project_nav_tab?(:container_registry))
- = nav_link controller: [:packages, :repositories, :infrastructure_registry] do
- = link_to packages_link, data: { qa_selector: 'packages_link' } do
- .nav-icon-container
- = sprite_icon('package')
- %span.nav-item-name
- = _('Packages & Registries')
- %ul.sidebar-sub-level-items
- = nav_link(controller: [:packages, :repositories, :infrastructure_registry], html_options: { class: "fly-out-top-item" } ) do
- = link_to packages_link do
- %strong.fly-out-top-item-name
- = _('Packages & Registries')
- %li.divider.fly-out-top-item
- - if project_nav_tab? :packages
- = nav_link controller: :packages do
- = link_to project_packages_path(@project), title: _('Package Registry') do
- %span= _('Package Registry')
- - if project_nav_tab? :container_registry
- = nav_link controller: :repositories do
- = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
- %span= _('Container Registry')
- - if project_nav_tab? :infrastructure_registry
- = nav_link controller: :infrastructure_registry do
- = link_to project_infrastructure_registry_index_path(@project), title: _('Infrastructure Registry') do
- %span= _('Infrastructure Registry')
diff --git a/app/views/layouts/nav/sidebar/_project_security_link.html.haml b/app/views/layouts/nav/sidebar/_project_security_link.html.haml
deleted file mode 100644
index 426845639e3..00000000000
--- a/app/views/layouts/nav/sidebar/_project_security_link.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- top_level_link = project_security_configuration_path(@project)
-- top_level_qa_selector = 'security_configuration_link'
-- if any_project_nav_tab?([:security_configuration])
- = nav_link(path: sidebar_security_paths) do
- = link_to top_level_link, data: { qa_selector: top_level_qa_selector } do
- .nav-icon-container
- = sprite_icon('shield')
- %span.nav-item-name
- = _('Security & Compliance')
-
- %ul.sidebar-sub-level-items
- = nav_link(path: sidebar_security_paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to top_level_link do
- %strong.fly-out-top-item-name
- = _('Security & Compliance')
-
- %li.divider.fly-out-top-item
- - if project_nav_tab?(:security_configuration)
- = nav_link(path: sidebar_security_configuration_paths) do
- = link_to project_security_configuration_path(@project), title: _('Configuration'), data: { qa_selector: 'security_configuration_link'} do
- %span= _('Configuration')
diff --git a/app/views/layouts/nav/sidebar/_tracing_link.html.haml b/app/views/layouts/nav/sidebar/_tracing_link.html.haml
deleted file mode 100644
index 7a31a20f5f0..00000000000
--- a/app/views/layouts/nav/sidebar/_tracing_link.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- return unless can?(current_user, :read_environment, @project)
-
-- if project_nav_tab? :settings
- = nav_link(controller: :tracings, action: [:show]) do
- = link_to project_tracing_path(@project), title: _('Tracing') do
- %span
- = _('Tracing')
diff --git a/app/views/layouts/simple_registration.html.haml b/app/views/layouts/simple_registration.html.haml
new file mode 100644
index 00000000000..dc7ec25c96e
--- /dev/null
+++ b/app/views/layouts/simple_registration.html.haml
@@ -0,0 +1,10 @@
+!!! 5
+%html{ lang: "en" }
+ = render "layouts/head"
+ %body.login-page.application.navless{ class: user_application_theme, data: { page: body_data_page } }
+ = render "layouts/header/logo_with_title"
+ = render "layouts/broadcast"
+ .container.navless-container.pt-0
+ .content.mw-460.mx-auto
+ = render "layouts/flash"
+ = yield
diff --git a/app/views/notify/change_in_merge_request_draft_status_email.html.haml b/app/views/notify/change_in_merge_request_draft_status_email.html.haml
index 5604a30d9f1..64ceb77e85c 100644
--- a/app/views/notify/change_in_merge_request_draft_status_email.html.haml
+++ b/app/views/notify/change_in_merge_request_draft_status_email.html.haml
@@ -1,2 +1,2 @@
-%p
- = _('%{username} changed the draft status of merge request %{mr_reference}' % {username: sanitize_name(@updated_by_user.name), mr_reference: @merge_request.to_reference })
+%p= html_escape(_('%{username} changed the draft status of merge request %{mr_link}')) % { username: link_to(@updated_by_user.name, user_url(@updated_by_user)),
+ mr_link: merge_request_reference_link(@merge_request) }
diff --git a/app/views/notify/in_product_marketing_email.html.haml b/app/views/notify/in_product_marketing_email.html.haml
index 015a12bbb6d..a1c3ecfb87e 100644
--- a/app/views/notify/in_product_marketing_email.html.haml
+++ b/app/views/notify/in_product_marketing_email.html.haml
@@ -163,43 +163,43 @@
%table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" }
%tr
%td{ align: "left", style: "padding: 0 20px;" }
- = about_link('mailers/in_product_marketing', 'gitlab-logo-gray-rgb.png', 200)
+ = about_link('mailers/in_product_marketing/gitlab-logo-gray-rgb.png', 200)
%tr
%td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" }
%tr{ style: "background-color: #ffffff;" }
%td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
%p
- = in_product_marketing_progress(@track, @series, format: :html).html_safe
+ = @message.progress.html_safe
%tr
%td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
- = in_product_marketing_logo(@track, @series)
+ = inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
%h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
- = in_product_marketing_title(@track, @series)
+ = @message.title
%h2{ style: "font-size: 28px; line-height: 34px; color: #000000; padding: 0; font-weight: 400;" }
- = in_product_marketing_subtitle(@track, @series)
+ = @message.subtitle
%tr
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
%p{ style: "margin: 0 0 20px 0;" }
- = in_product_marketing_body_line1(@track, @series, format: :html).html_safe
- - in_product_marketing_body_line2(@track, @series, format: :html)&.tap do |line|
+ = @message.body_line1.html_safe
+ - @message.body_line2&.tap do |line|
%p{ style: "margin: 0 0 20px 0;" }
= line.html_safe
%tr
%td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- .cta_link= cta_link(@track, @series, @group, format: :html)
+ .cta_link= @message.cta_link
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:75px 20px 25px;" }
- = about_link('', 'gitlab_logo.png', 80)
+ = about_link('gitlab_logo.png', 80)
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:0px ;" }
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:0px 10px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; " }
%span.footernav{ style: "color: #6e49cb; font-size: 16px; line-height: 26px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- = footer_links(format: :html).join('&nbsp;' * 3 + '|' + '&nbsp;' * 4).html_safe
+ = @message.footer_links.join('&nbsp;' * 3 + '|' + '&nbsp;' * 4).html_safe
%tr{ style: "background-color:#ffffff;" }
%td{ align: "center", style: "padding: 40px 30px 20px 30px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
- .address= address(format: :html)
+ .address= @message.address
%tr{ style: "background-color: #ffffff;" }
%td{ align: "left", style: "padding:20px 30px 20px 30px;" }
%span.footernav{ style: "color: #6e49cb; font-size: 14px; line-height: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#424242;" }
- = unsubscribe(@track, @series, format: :html).html_safe
+ = @message.unsubscribe.html_safe
diff --git a/app/views/notify/in_product_marketing_email.text.erb b/app/views/notify/in_product_marketing_email.text.erb
index bc8315e49a0..7d0fe7aec6d 100644
--- a/app/views/notify/in_product_marketing_email.text.erb
+++ b/app/views/notify/in_product_marketing_email.text.erb
@@ -1,14 +1,14 @@
-<%= in_product_marketing_tagline(@track, @series) %>
+<%= @message.tagline %>
-<%= in_product_marketing_title(@track, @series) %>
-<%= in_product_marketing_subtitle(@track, @series) %>
+<%= @message.title %>
+<%= @message.subtitle %>
-<%= in_product_marketing_body_line1(@track, @series) %>
+<%= @message.body_line1 %>
-<%= in_product_marketing_body_line2(@track, @series) %>
+<%= @message.body_line2 %>
-<%= cta_link(@track, @series, @group) %>
+<%= @message.cta_link %>
@@ -16,8 +16,8 @@
-<%= footer_links %>
+<%= @message.footer_links %>
-<%= address %>
+<%= @message.address %>
-<%= unsubscribe(@track, @series) %>
+<%= @message.unsubscribe %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index 7f0a50e9248..3219ee34736 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,5 +1,6 @@
%p.details
- #{link_to @issue.author_name, user_url(@issue.author)} created an issue #{issue_reference_link(@issue)}:
+ = html_escape(_('%{user} created an issue: %{issue_link}')) % { user: link_to(@issue.author_name, user_url(@issue.author)),
+ issue_link: issue_reference_link(@issue) }
- if @issue.assignees.any?
%p
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index bd61db3ee76..7c78d67316d 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -1,4 +1,5 @@
-<%= sanitize_name(@issue.author_name) %> <%= 'created an issue:' %> <%= url_for(project_issue_url(@issue.project, @issue)) %>
+<%= _('%{user} created an issue: %{issue_link}') % { user: sanitize_name(@issue.author_name),
+ issue_link: url_for(project_issue_url(@issue.project, @issue)) } %>
<%= assignees_label(@issue) if @issue.assignees.any? %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 8fdba10e7a1..c8a0a6591a6 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -1,8 +1,6 @@
%p.details
- = html_escape(_('%{userLinkStart}%{user}%{linkEnd} created a %{mrLinkStart}merge request%{linkEnd}:')) % {userLinkStart: "<a href=\"#{user_url(@merge_request.author)}\">".html_safe,
- user: @merge_request.author_name,
- mrLinkStart: "<a href=\"#{@target_url}\">".html_safe,
- linkEnd: '</a>'.html_safe}
+ = html_escape(_('%{user} created a merge request: %{mr_link}')) % { user: link_to(@merge_request.author_name, user_url(@merge_request.author)),
+ mr_link: merge_request_reference_link(@merge_request) }
%p
.branch
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 6148af4890e..09e8ca36225 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -1,7 +1,9 @@
-<%= sanitize_name(@merge_request.author_name) %> <%= 'created a merge request:' %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
+<%= _('%{user} created a merge request: %{mr_link}') % { user: sanitize_name(@merge_request.author_name),
+ mr_link: url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) }
+%>
<%= merge_path_description(@merge_request, 'to') %>
-<%= 'Author:' %> <%= @merge_request.author_name %>
+<%= "#{_('Author')}: #{sanitize_name(@merge_request.author_name)}" %>
<%= assignees_label(@merge_request) if @merge_request.assignees.any? %>
<%= reviewers_label(@merge_request) if @merge_request.reviewers.any? %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index ca895972b71..24c25bc1ab2 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -1,5 +1,5 @@
-- service = chat_name.service
-- project = service.project
+- integration = chat_name.integration
+- project = integration.project
%tr
%td
%strong
@@ -10,9 +10,9 @@
%td
%strong
- if can?(current_user, :admin_project, project)
- = link_to service.title, edit_project_service_path(project, service)
+ = link_to integration.title, edit_project_service_path(project, integration)
- else
- = service.title
+ = integration.title
%td
= chat_name.team_domain
%td
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 35335f3ef80..74b48115d0e 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -5,12 +5,12 @@
.form-group
= f.label :key, s_('Profiles|Key'), class: 'label-bold'
%p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.")
- = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
+ = f.text_area :key, class: "form-control gl-form-input js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
.form-row
.col.form-group
= f.label :title, _('Title'), class: 'label-bold'
- = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
- %p.form-text.text-muted= s_('Profiles|Give your individual key a title.')
+ = f.text_field :title, class: "form-control gl-form-input input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
+ %p.form-text.text-muted= s_('Profiles|Give your individual key a title. This will be publicly visible.')
.col.form-group
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 4eb321050ad..178ed01c766 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -28,4 +28,4 @@
%span.key-created-at.gl-display-flex.gl-align-items-center
- if key.can_delete?
.gl-ml-3
- = render 'shared/ssh_keys/key_delete', html_class: "btn gl-button btn-icon btn-danger js-confirm-modal-button", button_data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin))
+ = render 'shared/ssh_keys/key_delete', html_class: "btn gl-button btn-icon btn-default js-confirm-modal-button", button_data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin))
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index efcd0bb621f..7780ffe0cb4 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -16,16 +16,16 @@
.col-sm-2.col-form-label
= f.label :current_password, _('Current password')
.col-sm-10
- = f.password_field :current_password, required: true, class: 'form-control gl-form-input'
+ = f.password_field :current_password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
.form-group.row
.col-sm-2.col-form-label
= f.label :password, _('New password')
.col-sm-10
- = f.password_field :password, required: true, class: 'form-control gl-form-input'
+ = f.password_field :password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'new_password_field' }
.form-group.row
.col-sm-2.col-form-label
= f.label :password_confirmation, _('Password confirmation')
.col-sm-10
- = f.password_field :password_confirmation, required: true, class: 'form-control gl-form-input'
+ = f.password_field :password_confirmation, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
.form-actions
- = f.submit _('Set new password'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Set new password'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'set_new_password_button' }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 535964028f4..0adad6b64a0 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -100,7 +100,7 @@
.form-group
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
= f.number_field :tab_width,
- class: 'form-control',
+ class: 'form-control gl-form-input',
min: Gitlab::TabWidth::MIN,
max: Gitlab::TabWidth::MAX,
required: true
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 15544fb9c45..c3ec2f7bab3 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -70,10 +70,9 @@
prepend: emoji_button,
append: reset_message_button,
placeholder: s_("Profiles|What's your status?")
- - if Feature.enabled?(:set_user_availability_status, @user, default_enabled: :yaml)
- .checkbox-icon-inline-wrapper
- = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
- .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name')
+ .checkbox-icon-inline-wrapper
+ = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"]
+ .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name')
- if Feature.enabled?(:user_time_settings)
.col-lg-12
%hr
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index a9134057777..71262f4bcb9 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -29,7 +29,7 @@
- register_2fa_token = _('We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device.')
= register_2fa_token.html_safe
.row.gl-mb-3
- .col-md-4
+ .col-md-4.gl-pt-2{ style: 'background: #fff' }
= raw @qr_code
.col-md-8
.account-well
@@ -49,7 +49,7 @@
= @error
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
- = text_field_tag :pin_code, nil, class: "form-control", required: true, data: { qa_selector: 'pin_code_field' }
+ = text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index db0f13843dd..c5a0b6a1428 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -11,4 +11,4 @@
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/projects/_archived_notice.html.haml b/app/views/projects/_archived_notice.html.haml
index dcece8ab42f..5489e41d37b 100644
--- a/app/views/projects/_archived_notice.html.haml
+++ b/app/views/projects/_archived_notice.html.haml
@@ -2,4 +2,4 @@
.text-warning.center.prepend-top-20
%p
= sprite_icon('warning-solid')
- = _('Archived project! Repository and other project resources are read only')
+ = _('Archived project! Repository and other project resources are read-only')
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index 4b41231ba20..987ec74e4ba 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,7 +1,7 @@
-.form-actions
+.form-actions.gl-display-flex
= button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
= link_to 'Cancel', cancel_path,
- class: 'gl-button btn btn-default btn-cancel', data: {confirm: leave_edit_message}
+ class: 'gl-button btn btn-default gl-ml-3', data: {confirm: leave_edit_message}
= render 'shared/projects/edit_information'
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 0369ee50c40..8642dc5fc8c 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -7,7 +7,7 @@
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
#tree-holder.tree-holder.clearfix
- .nav-block
+ .nav-block.gl-display-flex.gl-align-items-center
= render 'projects/tree/tree_header', tree: @tree
#js-last-commit
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
index 9888ce417f8..55e609c0ffb 100644
--- a/app/views/projects/_fork_suggestion.html.haml
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -1,10 +1,7 @@
+- message_base = s_("ForkSuggestion|You can’t %{edit_start}edit%{edit_end} files directly in this project. Fork this project and submit a merge request with your changes.").html_safe
+- message = message_base.html_safe % { edit_start: '<span class="js-file-fork-suggestion-section-action">'.html_safe, edit_end: '</span>'.html_safe }
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
- %span.file-fork-suggestion-note
- You're not allowed to
- %span.js-file-fork-suggestion-section-action
- edit
- files in this project directly. Please fork this project,
- make your changes there, and submit a merge request.
- = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-confirm-secondary'
+ %span.file-fork-suggestion-note= message
+ = link_to s_('ForkSuggestion|Fork'), nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-confirm-secondary'
%button.js-cancel-fork-suggestion-button.gl-button.btn.btn-grouped{ type: 'button' }
- Cancel
+ = s_('ForkSuggestion|Cancel')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b2380a3ba57..a70679dab5f 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,6 +2,7 @@
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
- emails_disabled = @project.emails_disabled?
+- cache_enabled = Feature.enabled?(:cache_home_panel, @project, type: :development, default_enabled: :yaml)
.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
.gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3
@@ -23,42 +24,45 @@
- if current_user
%span.access-request-links.gl-ml-3
= render 'shared/members/access_request_links', source: @project
- - if @project.tag_list.present?
- %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center
- = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- - @project.topics_to_show.each do |topic|
- - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
- - explore_project_topic_path = explore_projects_path(tag: topic)
- - if topic.length > max_project_topic_length
- %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
- = topic.titleize
- - else
- %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
- = topic.titleize
+ - if @project.tag_list.present?
+ = cache_if(cache_enabled, [@project, :tag_list], expires_in: 1.day) do
+ %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center
+ = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- - if @project.has_extra_topics?
- .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil }
- = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown }
+ - @project.topics_to_show.each do |topic|
+ - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
+ - explore_project_topic_path = explore_projects_path(tag: topic)
+ - if topic.length > max_project_topic_length
+ %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic.titleize
+ - else
+ %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic.titleize
+ - if @project.has_extra_topics?
+ .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil }
+ = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown }
- .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
- - if current_user
- .gl-display-flex.gl-align-items-start.gl-mr-3
- - if @notification_setting
- .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
+ = cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do
+ .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
+ - if current_user
+ .gl-display-flex.gl-align-items-start.gl-mr-3
+ - if @notification_setting
+ .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
- .count-buttons.gl-display-flex.gl-align-items-flex-start
- = render 'projects/buttons/star'
- = render 'projects/buttons/fork'
+ .count-buttons.gl-display-flex.gl-align-items-flex-start
+ = render 'projects/buttons/star'
+ = render 'projects/buttons/fork'
- if can?(current_user, :download_code, @project)
- %nav.project-stats
- .nav-links.quick-links
- - if @project.empty_repo?
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- - else
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ = cache_if(cache_enabled, [@project, :download_code], expires_in: 1.minute) do
+ %nav.project-stats
+ .nav-links.quick-links
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
.home-panel-home-desc.mt-1
- if @project.description.present?
@@ -80,11 +84,12 @@
= render_if_exists "projects/home_mirror"
- if @project.badges.present?
- .project-badges.mb-2
- - @project.badges.each do |badge|
- %a.gl-mr-3{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: 'Project badge' }>
+ = cache_if(cache_enabled, [@project, :badges], expires_in: 1.day) do
+ .project-badges.mb-2
+ - @project.badges.each do |badge|
+ %a.gl-mr-3{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index e6ded3ad912..c0fe788b56a 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -83,7 +83,7 @@
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- = form_for @project, html: { class: 'new_project' } do |f|
+ = form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f|
%hr
= render "shared/import_form", f: f
= render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index f55d840e14b..2d18285ba80 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -22,7 +22,7 @@
= s_('ProjectSettings|When there is a merge conflict, the user is given the option to rebase.')
.form-check.mb-2
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio form-check-input", data: { qa_selector: 'merge_ff_radio_button' }
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio form-check-input", data: { qa_selector: 'merge_ff_radio' }
= label_tag :project_merge_method_ff, class: 'form-check-label' do
= s_('ProjectSettings|Fast-forward merge')
.text-secondary
diff --git a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
index 12ab905479a..6e3c366da82 100644
--- a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
@@ -3,7 +3,7 @@
.form-group
%b= s_('ProjectSettings|Merge suggestions')
%p.text-secondary
- - configure_the_commit_message_for_applied_suggestions_help_link_url = help_page_path('user/discussions/index.md', anchor: 'configure-the-commit-message-for-applied-suggestions')
+ - configure_the_commit_message_for_applied_suggestions_help_link_url = help_page_path('user/project/merge_requests/reviews/suggestions.md', anchor: 'configure-the-commit-message-for-applied-suggestions')
- configure_the_commit_message_for_applied_suggestions_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configure_the_commit_message_for_applied_suggestions_help_link_url }
= s_('ProjectSettings|The commit message used when applying merge request suggestions. %{link_start}Learn more about suggestions.%{link_end}').html_safe % { link_start: configure_the_commit_message_for_applied_suggestions_help_link_start, link_end: '</a>'.html_safe }
.mb-2
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 4695cd59f32..66fc313213a 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -8,7 +8,7 @@
.form-group.project-name.col-sm-12
= f.label :name, class: 'label-bold' do
%span= _("Project name")
- = f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
+ = f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
%span= s_("Project URL")
@@ -33,7 +33,7 @@
.form-group.project-path.col-sm-6
= f.label :path, class: 'label-bold' do
%span= _("Project slug")
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", required: true, aria: { required: true }
+ = f.text_field :path, placeholder: "my-awesome-project", class: "form-control gl-form-input", required: true, aria: { required: true }
- if current_user.can_create_group?
.form-text.text-muted
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
@@ -43,7 +43,7 @@
.form-group
= f.label :description, class: 'label-bold' do
= s_('ProjectsNew|Project description %{tag_start}(optional)%{tag_end}').html_safe % { tag_start: '<span>'.html_safe, tag_end: '</span>'.html_safe }
- = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
+ = f.text_area :description, placeholder: s_('ProjectsNew|Description format'), class: "form-control gl-form-input", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
= f.label :visibility_level, class: 'label-bold' do
= s_('ProjectsNew|Visibility Level')
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 84f2d352bc9..e50b964a253 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -12,7 +12,7 @@
- if @code_navigation_path
#js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } }
- if Feature.enabled?(:refactor_blob_viewer, @project, default_enabled: :yaml)
- #js-view-blob-app{ data: { blob_path: blob.path } }
+ #js-view-blob-app{ data: { blob_path: blob.path, project_path: @project.full_path } }
.gl-spinner-container
= loading_icon(size: 'md')
- else
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 7d3a0c4a026..f2f753b4e86 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -3,7 +3,7 @@
- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.gl-mb-3
- .js-file-title.file-title.align-items-center.clearfix{ data: { current_action: action } }
+ .js-file-title.file-title.gl-display-flex.gl-align-items-center.clearfix{ data: { current_action: action } }
.editor-ref.block-truncated.has-tooltip{ title: ref }
= sprite_icon('fork', size: 12)
= ref
@@ -26,16 +26,18 @@
dismiss_key: @project.id,
human_access: human_access } }
- .file-buttons
+ .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- if is_markdown
= render 'shared/blob/markdown_buttons', show_fullscreen_button: false
- = button_tag class: 'soft-wrap-toggle btn gl-button', type: 'button', tabindex: '-1' do
- %span.no-wrap
- = custom_icon('icon_no_wrap')
- No wrap
- %span.soft-wrap
- = custom_icon('icon_soft_wrap')
- Soft wrap
+ = button_tag class: 'soft-wrap-toggle btn gl-button btn-default', type: 'button', tabindex: '-1' do
+ .no-wrap
+ = sprite_icon('soft-unwrap', css_class: 'gl-button-icon')
+ %span.gl-button-text
+ No wrap
+ .soft-wrap
+ = sprite_icon('soft-wrap', css_class: 'gl-button-icon')
+ %span.gl-button-text
+ Soft wrap
.file-editor.code
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 24a4db010c8..a76e61bc3dd 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -11,8 +11,5 @@
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } )
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template] } } )
- - if experiment_enabled?(:ci_syntax_templates_b, subject: current_user) && @project.namespace.recent?
- .gitlab-ci-syntax-yml-selector.js-gitlab-ci-syntax-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Learn CI/CD syntax"), options: { toggle_class: 'js-gitlab-ci-syntax-yml-selector qa-gitlab-ci-syntax-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_syntax_ymls(@project) } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index c42b54ec61d..28e33e3ac9b 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -21,7 +21,7 @@
.form-actions
= button_tag class: 'btn gl-button btn-confirm btn-upload-file', id: 'submit-all', type: 'button' do
- .spinner.spinner-sm.gl-mr-2.js-loading-icon.hidden
+ .gl-spinner.gl-mr-2.js-loading-icon.hidden
= button_title
= link_to _("Cancel"), '#', class: "btn gl-button btn-default btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index abfed450316..9f89981e7ca 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,5 +1,7 @@
- breadcrumb_title _("Repository")
- page_title _("Edit"), @blob.path, @ref
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco')
- if @conflict
.gl-alert.gl-alert-danger.gl-mb-5.gl-mt-5
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 8722819fe4f..2aeffa88c8f 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,16 +1,19 @@
- breadcrumb_title _("Repository")
- page_title _("New File"), @path.presence, @ref
-%h3.page-title.blob-new-page-title
- New file
+%h3.page-title.blob-new-page-title#js-code-quality-walkthrough
+ = _('New file')
+ .js-code-quality-walkthrough{ data: { step: 'commit_ci_file' } }
.file-editor
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
+ - if params[:code_quality_walkthrough]
+ = hidden_field_tag 'code_quality_walkthrough', 'true'
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
- cancel_path: project_tree_path(@project, @id)
+ cancel_path: project_tree_path(@project, @id)
- if should_suggest_gitlab_ci_yml?
.js-suggest-gitlab-ci-yml-commit-changes{ data: { target: '#commit-changes',
merge_request_path: params[:mr_path],
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index c66300aa947..1ba38808937 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title "Repository"
- page_title @blob.path, @ref
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco', prefetch: true)
.js-signature-container{ data: { 'signatures-path': signatures_path } }
diff --git a/app/views/projects/blob/viewers/_changelog.html.haml b/app/views/projects/blob/viewers/_changelog.html.haml
index 80ead53beff..cac858c1444 100644
--- a/app/views/projects/blob/viewers/_changelog.html.haml
+++ b/app/views/projects/blob/viewers/_changelog.html.haml
@@ -1,4 +1,3 @@
= sprite_icon('history', css_class: 'gl-mr-1 gl-vertical-align-text-bottom')
= succeed '.' do
- To find the state of this project's repository at the time of any of these versions, check out
- = link_to "the tags", project_tags_path(viewer.project)
+ = _("To find the state of this project's repository at the time of any of these versions, check out %{link_start}the tags%{link_end}.").html_safe % { link_start: "<a href='#{project_tags_path(viewer.project)}'>".html_safe, link_end: "</a>".html_safe }
diff --git a/app/views/projects/blob/viewers/_contributing.html.haml b/app/views/projects/blob/viewers/_contributing.html.haml
index 18559e2908f..eac8c17b7ff 100644
--- a/app/views/projects/blob/viewers/_contributing.html.haml
+++ b/app/views/projects/blob/viewers/_contributing.html.haml
@@ -1,9 +1,9 @@
= sprite_icon('book')
-After you've reviewed these contribution guidelines, you'll be all set to
+= _("After you've reviewed these contribution guidelines, you'll be all set to")
- options = contribution_options(viewer.project)
- if options.any?
= succeed '.' do
= Gitlab::Utils.to_exclusive_sentence(options).html_safe
- else
- contribute to this project.
+ = _("contribute to this project.")
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index fda4b9c92cd..61f64177be8 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -4,4 +4,4 @@
%h1.light
= sprite_icon('download')
%h4
- Download (#{number_to_human_size(viewer.blob.raw_size)})
+ = _('Download (%{size})').html_safe % { size: number_to_human_size(viewer.blob.raw_size) }
diff --git a/app/views/projects/blob/viewers/_license.html.haml b/app/views/projects/blob/viewers/_license.html.haml
index d2bd90a898a..320d7dd4b9f 100644
--- a/app/views/projects/blob/viewers/_license.html.haml
+++ b/app/views/projects/blob/viewers/_license.html.haml
@@ -1,8 +1,6 @@
- license = viewer.license
= sprite_icon('scale')
-This project is licensed under the
-= succeed '.' do
- %strong= license.name
+= _("This project is licensed under the %{strong_start}%{license_name}%{strong_end}.").html_safe % { license_name: license.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
-= link_to 'Learn more', license.url, target: '_blank', rel: 'noopener noreferrer'
+= link_to _('Learn more'), license.url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index 86f59146cda..e06ff4edf71 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
= sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
= succeed '.' do
- To learn more about this project, read
- = link_to "the wiki", wiki_path(viewer.project.wiki)
+ - link_to_wiki = link_to(_("the wiki"), wiki_path(viewer.project.wiki))
+ = _("To learn more about this project, read %{link_to_wiki}.").html_safe % { link_to_wiki: link_to_wiki }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 3071e5ea5f8..2f89a3f62ed 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -4,9 +4,9 @@
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.inline>
%button.gl-button.btn.btn-default.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { qa_selector: 'download_source_code_button' } }
- = sprite_icon('download')
+ = sprite_icon('download', css_class: 'gl-icon')
%span.sr-only= _('Select Archive Format')
- = sprite_icon("chevron-down")
+ = sprite_icon('chevron-down', css_class: 'gl-icon')
.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%section
%h5.m-0.dropdown-bold-header= _('Download source code')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 5effa5a9e92..12ce4667e1a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -28,14 +28,14 @@
%li.dropdown-header= _('This repository')
- if can_push_code
- %li.qa-new-file-option= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
+ %li.qa-new-file-option= link_to _('New file'), project_new_blob_path(@project, @project.default_branch_or_main)
- unless @project.empty_repo?
%li= link_to _('New branch'), new_project_branch_path(@project)
%li= link_to _('New tag'), new_project_tag_path(@project)
- elsif can_collaborate_with_project?(@project)
- %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master')
+ %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch_or_main)
- elsif create_mr_from_new_fork
- - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'),
+ - continue_params = { to: project_new_blob_path(@project, @project.default_branch_or_main),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now }
- fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
diff --git a/app/views/projects/buttons/_remove_tag.html.haml b/app/views/projects/buttons/_remove_tag.html.haml
index cdf6336a259..58af0d91f30 100644
--- a/app/views/projects/buttons/_remove_tag.html.haml
+++ b/app/views/projects/buttons/_remove_tag.html.haml
@@ -2,5 +2,5 @@
- tag = local_assigns.fetch(:tag, nil)
- return unless project && tag
-%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-danger btn-icon has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } }
- = sprite_icon("remove")
+%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-default btn-icon has-tooltip gl-ml-3\! #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } }
+ = sprite_icon('remove', css_class: 'gl-icon')
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index eb588e150f7..674765e9f89 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -1,3 +1,5 @@
- page_title s_('Pipelines|Pipeline Editor')
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco')
#js-pipeline-editor{ data: js_pipeline_editor_data(@project) }
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 1b28136e82c..67007aa7448 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -37,13 +37,13 @@
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
.commit-info.branches
- .spinner.vertical-align-middle
+ .gl-spinner.vertical-align-middle
.well-segment.merge-request-info
.icon-container
= custom_icon('mr_bold')
%span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) }
- .spinner.vertical-align-middle
+ .gl-spinner.vertical-align-middle
- if can?(current_user, :read_pipeline, @last_pipeline)
.well-segment.pipeline-info
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 7f52e6ed7eb..16df743475d 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,7 +1,11 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
+- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
+
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"project-id": @project.id,
+ "artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
+ "artifacts-endpoint-placeholder" => artifacts_endpoint_placeholder,
} }
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 5652b503a6d..c3fdfeb6f4e 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -12,7 +12,12 @@
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
= render "ci_menu"
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-commit", paginate_diffs: true
+ = render "projects/diffs/diffs",
+ diffs: @diffs,
+ environment: @environment,
+ diff_page_context: "is-commit",
+ paginate_diffs: true,
+ paginate_diffs_per_page: Projects::CommitController::COMMIT_DIFFS_PER_PAGE
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index ceb312450be..bc0d14743b9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -3,22 +3,24 @@
- `assets/javascripts/diffs/components/commit_item.vue`
EXCEPTION WARNING - see above `.vue` file for de-sync drift
--#-----------------------------------------------------------------
-- view_details = local_assigns.fetch(:view_details, false)
-- merge_request = local_assigns.fetch(:merge_request, nil)
-- project = local_assigns.fetch(:project) { merge_request&.project }
-- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
-- commit = commit.present(current_user: current_user)
-- commit_status = commit.status_for(ref)
-- collapsible = local_assigns.fetch(:collapsible, true)
-- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
-
-- link = commit_path(project, commit, merge_request: merge_request)
+ WARNING: When introducing new content here, please consider what
+ changes may need to be made in the cache keys used to
+ wrap this view, found in
+ CommitsHelper#commit_partial_cache_key
+-#-----------------------------------------------------------------
+- view_details = local_assigns.fetch(:view_details, false)
+- merge_request = local_assigns.fetch(:merge_request, nil)
+- project = local_assigns.fetch(:project) { merge_request&.project }
+- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- commit = commit.present(current_user: current_user)
+- commit_status = commit.status_for(ref)
+- collapsible = local_assigns.fetch(:collapsible, true)
+- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
+- link = commit_path(project, commit, merge_request: merge_request)
- show_project_name = local_assigns.fetch(:show_project_name, false)
%li{ class: ["commit flex-row", ("js-toggle-container" if collapsible)], id: "commit-#{commit.short_id}" }
-
.avatar-cell.d-none.d-sm-block
= author_avatar(commit, size: 40, has_tooltip: false)
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 9e2dca3ad71..e6c9a7166a9 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -3,8 +3,8 @@
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
-- commits = @commits
-- context_commits = @context_commits
+- commits = @commits&.map { |commit| commit.present(current_user: current_user) }
+- context_commits = @context_commits&.map { |commit| commit.present(current_user: current_user) }
- hidden = @hidden_commit_count
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
@@ -14,7 +14,10 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
- = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+ - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ - else
+ = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if context_commits.present?
%li.commit-header.js-commit-header
@@ -25,7 +28,10 @@
%li.commits-row
%ul.content-list.commit-list.flex-list
- = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+ - if Feature.enabled?(:cached_commits, project, default_enabled: :yaml)
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ - else
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.gl-alert.gl-alert-warning
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index e3ab184ec6f..426d022da26 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -4,11 +4,11 @@
%h3.page-title
= _("Compare Git revisions")
.sub-header-block
- - example_master = capture do
- %code.ref-name master
+ - example_branch = capture do
+ %code.ref-name= @project.default_branch_or_main
- example_sha = capture do
%code.ref-name 4eedf23
- = html_escape(_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { master: example_master.html_safe, sha: example_sha.html_safe }
+ = html_escape(_("Choose a branch/tag (e.g. %{branch}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { branch: example_branch.html_safe, sha: example_sha.html_safe }
%br
= html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 9e9c271e7be..1fc067b6be1 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -6,8 +6,15 @@
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) }
- if @commits.present?
- = render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, diff_page_context: "is-compare"
+ -# Only show commit list in the first page
+ - hide_commit_list = params[:page].present? && params[:page] != '1'
+ = render "projects/commits/commit_list" unless hide_commit_list
+ = render "projects/diffs/diffs",
+ diffs: @diffs,
+ environment: @environment,
+ diff_page_context: "is-compare",
+ paginate_diffs: true,
+ paginate_diffs_per_page: Projects::CompareController::COMMIT_DIFFS_PER_PAGE
- else
.card.bg-light
.center
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 1c7a9ffe0bb..bb2682bb7c0 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -4,7 +4,8 @@
- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
- load_diff_files_async = Feature.enabled?(:async_commit_diff_files, @project) && diff_page_context == "is-commit"
- paginate_diffs = local_assigns.fetch(:paginate_diffs, false) && !load_diff_files_async
-- diff_files = conditionally_paginate_diff_files(diffs, paginate: paginate_diffs)
+- paginate_diffs_per_page = local_assigns.fetch(:paginate_diffs_per_page, nil)
+- diff_files = conditionally_paginate_diff_files(diffs, paginate: paginate_diffs, per: paginate_diffs_per_page)
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
@@ -33,7 +34,7 @@
- url = url_for(safe_params.merge(action: 'diff_files'))
.js-diffs-batch{ data: { diff_files_path: url } }
.text-center
- %span.spinner.spinner-md
+ %span.gl-spinner.gl-spinner-md
- else
= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index ecaf3467cd2..187fe608a68 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -106,7 +106,7 @@
.save-project-loader.hide
.center
%h2
- .spinner.spinner-md.align-text-bottom
+ .gl-spinner.gl-spinner-md.align-text-bottom
= _('Saving project.')
%p= _('Please wait a moment, this page will automatically refresh when ready.')
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 171222368d6..b76f6b27aa8 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- default_branch_name = @project.default_branch_or_master
+- default_branch_name = @project.default_branch_or_main
- @skip_current_level_breadcrumb = true
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 06a2ed46805..0136184f80d 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -7,4 +7,4 @@
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments/index.md"),
"project-path" => @project.full_path,
- "default-branch-name" => @project.default_branch_or_master } }
+ "default-branch-name" => @project.default_branch_or_main } }
diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml
index 028595aba0b..1549f5cf6d6 100644
--- a/app/views/projects/feature_flags/edit.html.haml
+++ b/app/views/projects/feature_flags/edit.html.haml
@@ -12,5 +12,5 @@
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'),
+ environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } }
diff --git a/app/views/projects/feature_flags/new.html.haml b/app/views/projects/feature_flags/new.html.haml
index 3bad1d9773c..bc52f52ecf7 100644
--- a/app/views/projects/feature_flags/new.html.haml
+++ b/app/views/projects/feature_flags/new.html.haml
@@ -10,5 +10,5 @@
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments', anchor: 'scoping-environments-with-specs'),
+ environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
project_id: @project.id } }
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index a8a4eef65b3..ee4dbf5c05c 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -4,7 +4,7 @@
Recent Deliveries
%p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
.col-lg-9
- - if hook_logs.any?
+ - if hook_logs.present?
%table.table
%thead
%tr
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 838b4538cad..9c01d93f7d0 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -3,7 +3,9 @@
.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"
+ - checkbox_id = dom_id(issue, "selected")
+ %label.gl-sr-only{ for: checkbox_id }= issue.title
+ = check_box_tag checkbox_id, nil, false, 'data-id' => issue.id, class: "selected-issuable"
.issuable-main-info
.issue-title.title
%span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index ef602da72e5..e4d072a9472 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,4 +1,5 @@
- is_project_overview = local_assigns.fetch(:is_project_overview, false)
+= render 'shared/alerts/positioning_disabled'
- if Feature.enabled?(:vue_issuables_list, @project) && !is_project_overview
- data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
@@ -15,7 +16,7 @@
'scoped-labels-available': scoped_labels_available?(@project).to_json } }
- else
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
- %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
+ %ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class }
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
= render empty_state_path
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 45b2f86c03d..07fec195899 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -13,13 +13,13 @@
.create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
.btn-group.unavailable
%button.gl-button.btn{ type: 'button', disabled: 'disabled' }
- .spinner.align-text-bottom.gl-button-icon.hide
+ .gl-spinner.align-text-bottom.gl-button-icon.hide
%span.text
Checking branch availability…
.btn-group.available.hidden
%button.gl-button.btn.js-create-merge-request.btn-confirm{ type: 'button', data: { action: data_action } }
- .spinner.js-spinner.gl-mr-2.gl-display-none
+ .gl-spinner.js-spinner.gl-mr-2.gl-display-none
= value
%button.gl-button.btn.btn-confirm.btn-icon.dropdown-toggle.create-merge-request-dropdown-toggle.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } }
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 9b043ea3c47..3e8442eee86 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -29,7 +29,7 @@
.issues-holder
= render 'issues'
- if new_issue_email
- .issuable-footer.text-center
+ .gl-text-center.gl-pt-5.gl-pb-7
.js-issueable-by-email{ data: { initial_email: new_issue_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
- new_project_issue_button_path = @project.archived? ? false : new_project_issue_path(@project)
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index f2aab3d9394..1da3881c104 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -2,7 +2,7 @@
- add_page_specific_style 'page_bundles/ci_status'
- if Feature.enabled?(:jobs_table_vue, @project, default_enabled: :yaml)
- #js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json } }
+ #js-jobs-table{ data: { full_path: @project.full_path, job_counts: job_counts.to_json, job_statuses: job_statuses.to_json, pipeline_editor_path: project_ci_pipeline_editor_path(@project), empty_state_svg_path: image_path('jobs-empty-state.svg') } }
- else
.top-area
- build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml
index 94023b21aab..4935b72d3fa 100644
--- a/app/views/projects/learn_gitlab/index.html.haml
+++ b/app/views/projects/learn_gitlab/index.html.haml
@@ -2,4 +2,4 @@
- page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab'
-#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } }
+#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } }
diff --git a/app/views/projects/merge_requests/_description.html.haml b/app/views/projects/merge_requests/_description.html.haml
index c20479662dd..1dd4cc6495c 100644
--- a/app/views/projects/merge_requests/_description.html.haml
+++ b/app/views/projects/merge_requests/_description.html.haml
@@ -1,6 +1,6 @@
%div
- if @merge_request.description.present?
- .description.qa-description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' }
+ .description{ class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : '' , data: { qa_selector: 'description_content' } }
.md
= markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field{ data: { value: @merge_request.description } }
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 6c0fc9575fc..b70bc740175 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,7 +1,9 @@
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @can_bulk_update
.issue-check.hidden
- = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected-issuable"
+ - checkbox_id = dom_id(merge_request, "selected")
+ %label.gl-sr-only{ for: checkbox_id }= merge_request.title
+ = check_box_tag checkbox_id, nil, false, 'data-id' => merge_request.id, class: "selected-issuable"
.issuable-info-container
.issuable-main-info
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index c38cf62b36c..916b841e350 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,3 +1,3 @@
.detail-page-description.py-2
- %h2.title.qa-title.mb-0
+ %h2.title.mb-0{ data: { qa_selector: 'title_content' } }
= markdown_field(@merge_request, :title)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 26d8e571973..e42032fef66 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -1,53 +1,51 @@
- @no_breadcrumb_border = true
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
-- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
+- cache_key = [@project, @merge_request, can_update_merge_request, can_reopen_merge_request, are_close_and_open_buttons_hidden]
-- if @merge_request.closed_or_merged_without_fork?
- .gl-alert.gl-alert-danger.gl-mb-5
- = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-body
- The source project of this merge request has been removed.
+= cache_if(Feature.enabled?(:cached_mr_title, @project, default_enabled: :yaml), cache_key, expires_in: 1.day) do
+ - if @merge_request.closed_or_merged_without_fork?
+ .gl-alert.gl-alert-danger.gl-mb-5
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ The source project of this merge request has been removed.
-.detail-page-header.border-bottom-0.pt-0.pb-0
- .detail-page-header-body
- .issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state } }
- = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block
- = state_human_name
+ .detail-page-header.border-bottom-0.pt-0.pb-0
+ .detail-page-header-body
+ = render "shared/issuable/status_box", issuable: @merge_request
- .issuable-meta
- #js-issuable-header-warnings
- = issuable_meta(@merge_request, @project)
+ .issuable-meta
+ #js-issuable-header-warnings
+ = issuable_meta(@merge_request, @project)
- %a.gl-button.btn.btn-default.btn-icon.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
- = sprite_icon('chevron-double-lg-left')
+ %a.gl-button.btn.btn-default.btn-icon.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = sprite_icon('chevron-double-lg-left')
- .detail-page-header-actions.js-issuable-actions
- .clearfix.dropdown
- %button.gl-button.btn.btn-default.float-left.gl-md-display-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
- Options
- = sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
- .dropdown-menu.dropdown-menu-right
- %ul
- - if can_update_merge_request
- %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- - if @merge_request.opened?
- %li
- = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
- %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- - if can_reopen_merge_request
- %li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
- - unless @merge_request.merged? || current_user == @merge_request.author
- %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
+ .detail-page-header-actions.js-issuable-actions
+ .clearfix.dropdown
+ %button.gl-button.btn.btn-default.float-left.gl-md-display-none.gl-w-full{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = sprite_icon('chevron-down', css_class: 'gl-text-gray-500')
+ .dropdown-menu.dropdown-menu-right
+ %ul
+ - if can_update_merge_request
+ %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ - if @merge_request.opened?
+ %li
+ = link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
+ %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
+ - if can_reopen_merge_request
+ %li{ class: merge_request_button_visibility(@merge_request, false) }
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
+ - unless @merge_request.merged? || current_user == @merge_request.author
+ %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- - if can_update_merge_request
- = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
+ - if can_update_merge_request
+ = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
- - if can_update_merge_request && !are_close_and_open_buttons_hidden
- = render 'projects/merge_requests/close_reopen_draft_report_toggle'
- - elsif !@merge_request.merged?
- = link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default float-right gl-ml-3', title: _('Report abuse')
+ - if can_update_merge_request && !are_close_and_open_buttons_hidden
+ = render 'projects/merge_requests/close_reopen_draft_report_toggle'
+ - elsif !@merge_request.merged?
+ = link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'gl-display-none gl-md-display-block gl-button btn btn-default float-right gl-ml-3', title: _('Report abuse')
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 6e6046eba14..606442d71a9 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -1,11 +1,15 @@
+- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
+
= javascript_tag do
:plain
window.gl = window.gl || {};
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
+ window.gl.mrWidgetData.artifacts_endpoint = '#{downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json)}';
+ window.gl.mrWidgetData.artifacts_endpoint_placeholder = '#{artifacts_endpoint_placeholder}';
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
- window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
+ window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviews/index.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests')}';
window.gl.mrWidgetData.license_compliance_docs_path = '#{help_page_path('user/compliance/license_compliance/index.md', anchor: 'policies')}';
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 7082bf4b8b0..b99714c1794 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -30,7 +30,7 @@
= dropdown_loading
.card-footer
.text-center
- .js-source-loading.mt-1.spinner.spinner-sm
+ .js-source-loading.mt-1.gl-spinner
%ul.list-unstyled.mr_source_commit
.col-lg-6
@@ -59,7 +59,7 @@
= dropdown_loading
.card-footer
.text-center
- .js-target-loading.mt-1.spinner.spinner-sm
+ .js-target-loading.mt-1.gl-spinner
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index a8facf1c6fd..7e1ca19d9b6 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -26,16 +26,16 @@
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
Commits
- %span.badge.badge-pill= @total_commit_count
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @total_commit_count
- if @pipelines.any?
%li.builds-tab
= link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do
Pipelines
- %span.badge.badge-pill= @pipelines.size
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @pipelines.size
%li.diffs-tab
= link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', qa_selector: 'diffs_tab'} do
Changes
- %span.badge.badge-pill= @merge_request.diff_size
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @merge_request.diff_size
#diff-notes-app.tab-content
#new.commits.tab-pane.active
@@ -48,4 +48,4 @@
.mr-loading-status
.loading.hide
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 22d78418c5b..289f88c9705 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -22,7 +22,7 @@
.merge-requests-holder
= render 'merge_requests'
- if new_merge_request_email
- .issuable-footer.text-center
+ .gl-text-center.gl-pt-5.gl-pb-7
.js-issueable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions'), markdown_help_path: help_page_path('user/markdown'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } }
- else
= render 'shared/empty_states/merge_requests', button_path: new_merge_request_path
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 416cb932ec9..49f2795538c 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -5,7 +5,7 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
- page_description @merge_request.description_html
- page_card_attributes @merge_request.card_attributes
-- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
+- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions.md')
- number_of_pipelines = @pipelines.size
- mr_action = j(params[:tab].presence || 'show')
- add_page_specific_style 'page_bundles/merge_requests'
@@ -40,7 +40,7 @@
= render "projects/merge_requests/tabs/tab", name: "diffs", class: "diffs-tab", id: "diffs-tab", qa_selector: "diffs_tab" do
= tab_link_for @merge_request, :diffs do
= _("Changes")
- %span.badge.badge-pill.gl-badge.badge-muted.sm= @merge_request.diff_size
+ %span.badge.badge-pill.gl-badge.badge-muted.sm= @diffs_count
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
#js-vue-discussion-counter
@@ -85,7 +85,7 @@
.mr-loading-status
.loading.hide
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 56906eb6e66..dfb9defb91c 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -7,13 +7,17 @@
.col-form-label.col-sm-2
= f.label :title, _('Title')
.col-sm-10
- = f.text_field :title, maxlength: 255, class: 'form-control', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true
+ = f.text_field :title, maxlength: 255, class: 'form-control gl-form-input', data: { qa_selector: 'milestone_title_field' }, required: true, autofocus: true
.form-group.row.milestone-description
.col-form-label.col-sm-2
= f.label :description, _('Description')
.col-sm-10
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project) } do
- = render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', qa_selector: 'milestone_description_field', placeholder: _('Write milestone description...')
+ = render 'shared/zen', f: f, attr: :description,
+ classes: 'note-textarea',
+ qa_selector: 'milestone_description_field',
+ supports_autocomplete: true,
+ placeholder: _('Write milestone description...')
= render 'shared/notes/hints'
.clearfix
.error-alert
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 2185df3a994..5a6c2c5faaf 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -4,6 +4,8 @@
- page_description @milestone.description_html
- add_page_specific_style 'page_bundles/milestone'
+- add_page_startup_api_call milestone_tab_path(@milestone, 'issues', show_project_name: false)
+
= render 'shared/milestones/header', milestone: @milestone
= render 'shared/milestones/description', milestone: @milestone
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index 4e3cd609d75..4411bc474b8 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -4,7 +4,7 @@
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless mirror.ssh_mirror_url?) }
%button.btn.gl-button.btn-inverted.btn-secondary.inline.js-detect-host-keys.gl-mr-3{ type: 'button', data: { qa_selector: 'detect_host_keys' } }
- .js-spinner.d-none.spinner.mr-1
+ .js-spinner.d-none.gl-spinner.mr-1
= _('Detect host keys')
.fingerprint-ssh-info.js-fingerprint-ssh-info.gl-mt-3.gl-mb-3{ class: ('collapse' unless mirror.ssh_mirror_url?) }
%label.label-bold
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 3cff85a4979..4cabb930433 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -17,4 +17,4 @@
- if @commit
.network-graph.gl-bg-white.gl-overflow-scroll.gl-overflow-x-hidden{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
.text-center.gl-mt-3
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 059d6eb28c5..c62853145b6 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -2,80 +2,38 @@
- @hide_top_links = true
- page_title _('New Project')
- header_title _("Projects"), dashboard_projects_path
-- active_tab = local_assigns.fetch(:active_tab, 'blank')
+- add_page_specific_style 'page_bundles/new_namespace'
.project-edit-container.gl-mt-5
.project-edit-errors
= render 'projects/errors'
- .js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any?, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
+ .js-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?).to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects") } }
.row{ 'v-cloak': true }
- .col-lg-3.profile-settings-sidebar
- %h4.gl-mt-0
- = _('New project')
- %p
- - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "project-features"), target: '_blank'
- = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
- %p
- = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
- = render_if_exists 'projects/new_ci_cd_banner_external_repo'
- %p
- - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/index", anchor: "getting-started"), target: '_blank'
- = _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
- .md
- = brand_new_project_guidelines
- %p
- %strong= _("Tip:")
- = _("You can also create a project from the command line.")
-
- .col-lg-9.js-toggle-container
- %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', experiment_track_label: 'blank_project' }, role: 'tab' }
- %span.d-none.d-sm-block= s_('ProjectsNew|Blank project')
- %span.d-block.d-sm-none= s_('ProjectsNew|Blank')
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', experiment_track_label: 'create_from_template' }, role: 'tab' }
- %span.d-none.d-sm-block.qa-project-create-from-template-tab= s_('ProjectsNew|Create from template')
- %span.d-block.d-sm-none= s_('ProjectsNew|Template')
- %li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', experiment_track_label: 'import_project' }, role: 'tab' }
- %span.d-none.d-sm-block= s_('ProjectsNew|Import project')
- %span.d-block.d-sm-none= s_('ProjectsNew|Import')
- = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
-
- .tab-content.gitlab-tab-content
- .tab-pane.js-toggle-container{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
- = form_for @project, html: { class: 'new_project' } do |f|
- = render 'new_project_fields', f: f, project_name_id: "blank-project-name"
-
- #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
- .card.card-slim.m-4.p-4
+ #blank-project-pane.tab-pane.active
+ = form_for @project, html: { class: 'new_project' } do |f|
+ = render 'new_project_fields', f: f, project_name_id: "blank-project-name"
+
+ #create-from-template-pane.tab-pane
+ .gl-card.gl-my-5
+ .gl-card-body
+ %div
+ - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
+ = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ = form_for @project, html: { class: 'new_project' } do |f|
+ .project-template
+ .form-group
%div
- - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
- = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- = form_for @project, html: { class: 'new_project' } do |f|
- .project-template
- .form-group
- %div
- = render 'project_templates', f: f, project: @project
-
- .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- - if import_sources_enabled?
- = render 'import_project_pane', active_tab: active_tab
- - else
- .nothing-here-block
- %h4= s_('ProjectsNew|No import options available')
- %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
+ = render 'project_templates', f: f, project: @project
- = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
+ #import-project-pane.tab-pane.js-toggle-container
+ - if import_sources_enabled?
+ = render 'import_project_pane'
+ - else
+ .nothing-here-block
+ %h4= s_('ProjectsNew|No import options available')
+ %p= s_('ProjectsNew|Contact an administrator to enable options for importing your project.')
-.save-project-loader.d-none
- .center
- %h2
- .spinner.spinner-md.align-text-bottom
- = s_('ProjectsNew|Creating project & repository.')
- %p
- = s_('ProjectsNew|Please wait a moment, this page will automatically refresh when ready.')
+ = render_if_exists 'projects/new_ci_cd_only_project_pane'
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index 61f6ad34052..f69041e1eb1 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -2,17 +2,17 @@
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
- %span.badge.badge-pill.js-totalbuilds-count
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm.js-totalbuilds-count
= number_with_delimiter(all_schedules.count(:id))
%li{ class: active_when(scope == 'active') }>
= link_to schedule_path_proc.call('active') do
= s_("PipelineSchedules|Active")
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= number_with_delimiter(all_schedules.active.count(:id))
%li{ class: active_when(scope == 'inactive') }>
= link_to schedule_path_proc.call('inactive') do
= s_("PipelineSchedules|Inactive")
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= number_with_delimiter(all_schedules.inactive.count(:id))
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index f0b2349c493..e56a240c487 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -11,16 +11,16 @@
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _('Jobs')
- %span.badge.badge-pill.js-builds-counter= pipeline.total_size
+ %span.badge.badge-pill.gl-badge.badge-muted.sm.js-builds-counter= pipeline.total_size
- if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _('Failed Jobs')
- %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
+ %span.badge.badge-pill.gl-badge.badge-muted.sm.js-failures-counter= @pipeline.failed_builds.count
%li.js-tests-tab-link
= link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
= s_('TestReports|Tests')
- %span.badge.badge-pill.js-test-report-badge-counter= @pipeline.test_report_summary.total[:count]
+ %span.badge.badge-pill.gl-badge.badge-muted.sm.js-test-report-badge-counter= @pipeline.test_report_summary.total[:count]
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
@@ -83,5 +83,7 @@
#js-tab-tests.tab-pane
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
- blob_path: project_blob_path(@project, @pipeline.sha) } }
+ blob_path: project_blob_path(@project, @pipeline.sha),
+ has_test_report: @pipeline.has_reports?(Ci::JobArtifact.test_reports).to_s,
+ empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg') } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 4b0487f4685..42bb8117766 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -1,12 +1,15 @@
- page_title _('Pipelines')
- add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/ci_status'
+- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
-#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
+#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]),
project_id: @project.id,
params: params.to_json,
+ "artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
+ "artifacts-endpoint-placeholder" => artifacts_endpoint_placeholder,
"pipeline-schedule-url" => pipeline_schedules_path(@project),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
@@ -17,4 +20,5 @@
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project),
"has-gitlab-ci" => has_gitlab_ci?(@project).to_s,
"add-ci-yml-path" => can?(current_user, :create_pipeline, @project) && @project.present(current_user: current_user).add_ci_yml_path,
- "suggested-ci-templates" => experiment_suggested_ci_templates.to_json } }
+ "suggested-ci-templates" => experiment_suggested_ci_templates.to_json,
+ "code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path } }
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 14de982e239..e92f14fcc63 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,55 +1,17 @@
- breadcrumb_title _('Pipelines')
- page_title s_('Pipeline|Run pipeline')
-- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
= s_('Pipeline|Run pipeline')
%hr
-- if Feature.enabled?(:new_pipeline_form, @project, default_enabled: :yaml)
- #js-new-pipeline{ data: { project_id: @project.id,
- pipelines_path: project_pipelines_path(@project),
- config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),
- default_branch: @project.default_branch,
- ref_param: params[:ref] || @project.default_branch,
- var_param: params[:var].to_json,
- file_param: params[:file_var].to_json,
- project_refs_endpoint: refs_project_path(@project, sort: 'updated_desc'),
- settings_link: project_settings_ci_cd_path(@project),
- max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
-
-- else
- = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
- = form_errors(@pipeline)
- = pipeline_warnings(@pipeline)
- .form-group.row
- .col-sm-12
- = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
- = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
- = dropdown_tag(params[:ref] || @project.default_branch,
- options: { toggle_class: 'js-branch-select wide monospace',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
- data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .form-text.text-muted
- = s_("Pipeline|Existing branch name or tag")
-
- .col-sm-12.gl-mt-3.js-ci-variable-list-section
- %label
- = s_('Pipeline|Variables')
- %ul.ci-variable-list
- - if params[:var]
- - params[:var].each do |variable|
- = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
- - if params[:file_var]
- - params[:file_var].each do |variable|
- - variable.push("file")
- = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
- = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
- .form-text.text-muted
- = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
-
- .form-actions
- = f.submit s_('Pipeline|Run pipeline'), class: 'btn gl-button btn-confirm gl-mr-3 js-variables-save-button'
- = link_to _('Cancel'), project_pipelines_path(@project), class: 'btn gl-button btn-default'
-
- %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
+#js-new-pipeline{ data: { project_id: @project.id,
+ pipelines_path: project_pipelines_path(@project),
+ config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),
+ default_branch: @project.default_branch,
+ ref_param: params[:ref] || @project.default_branch,
+ var_param: params[:var].to_json,
+ file_param: params[:file_var].to_json,
+ project_refs_endpoint: refs_project_path(@project, sort: 'updated_desc'),
+ settings_link: project_settings_ci_cd_path(@project),
+ max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 98b1c5adcb5..93b0a525191 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -11,6 +11,10 @@
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
+
+ - if @pipeline.failed? && @pipeline.user_not_verified?
+ #js-cc-validation-required-alert
+
- if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 22bf61b6873..0fa9fb7079b 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -4,7 +4,7 @@
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
- - if can_invite_members_for_project?(@project)
+ - if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
.row
.col-md-12.col-lg-6.gl-display-flex
.gl-flex-direction-column.gl-flex-wrap.align-items-baseline
@@ -18,8 +18,15 @@
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
- .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
- .js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
+ - if can_import_members?
+ = link_to _("Import a project"),
+ import_project_project_members_path(@project),
+ class: "btn btn-default btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
+ title: _("Import members from another project")
+ - if @project.allowed_to_share_with_group?
+ .js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
+ - if !membership_locked?
+ .js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'projects/invite_members_modal', project: @project
- else
@@ -32,7 +39,7 @@
%p
= html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
- - if !can_invite_members_for_project?(@project) && can_manage_project_members?(@project) && project_can_be_shared?
+ - if Feature.disabled?(:invite_members_group_modal, @project.group) && can_manage_project_members?(@project) && project_can_be_shared?
- if !membership_locked? && @project.allowed_to_share_with_group?
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
@@ -75,22 +82,21 @@
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
- .js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
+ .js-project-members-list{ data: { members_data: project_members_list_data_json(@project, @project_members, { param_name: :page, params: { search_groups: nil } }) } }
.loading
- .spinner.spinner-md
- = paginate @project_members, theme: "gitlab", params: { search_groups: nil }
+ .gl-spinner.gl-spinner-md
- if show_groups?(@group_links)
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
- .js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
+ .js-project-group-links-list{ data: { members_data: project_group_links_list_data_json(@project, @group_links) } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- if show_invited_members?(@project, @invited_members)
#tab-invited-members.tab-pane
- .js-project-invited-members-list{ data: project_members_list_data_attributes(@project, @invited_members) }
+ .js-project-invited-members-list{ data: { members_data: project_members_list_data_json(@project, @invited_members) } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- if show_access_requests?(@project, @requesters)
#tab-access-requests.tab-pane
- .js-project-access-requests-list{ data: project_members_list_data_attributes(@project, @requesters) }
+ .js-project-access-requests-list{ data: { members_data: project_members_list_data_json(@project, @requesters) } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/projects/project_templates/_template.html.haml b/app/views/projects/project_templates/_template.html.haml
index e2bfd0881b5..827ff62f8c3 100644
--- a/app/views/projects/project_templates/_template.html.haml
+++ b/app/views/projects/project_templates/_template.html.haml
@@ -10,7 +10,7 @@
.controls.d-flex.align-items-center
%a.btn.gl-button.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_event: "click_button", track_value: "" } }
= _("Preview")
- %label.btn.gl-button.btn-success.template-button.choose-template.gl-mb-0{ for: template.name }
+ %label.btn.gl-button.btn-confirm.template-button.choose-template.gl-mb-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_event: "click_button", track_value: "" } }
%span{ data: { qa_selector: 'use_template_button' } }
= _("Use template")
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index bbef5150a62..f56fd7f557d 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -12,7 +12,7 @@
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
"registry_host_url_with_port" => escape_once(registry_config.host_port),
- "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
+ "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index.md', anchor: 'cleanup-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"project_path": @project.full_path,
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index b37b530c33f..5d737bb3901 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -13,7 +13,7 @@
%br
%br
- if @project.group_runners_enabled?
- = link_to toggle_group_runners_project_runners_path(@project), class: 'btn gl-button btn-warning-secondary', method: :post do
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn gl-button btn-default', method: :post do
= _('Disable group runners')
- else
= link_to toggle_group_runners_project_runners_path(@project), class: 'btn gl-button btn-confirm-secondary', method: :post do
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index fccfca38013..3df4f3a0bd0 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,23 +1,6 @@
-- isVueifySharedRunnersToggleEnabled = Feature.enabled?(:vueify_shared_runners_toggle, @project)
+= render 'shared/runners/shared_runners_description'
-= render layout: 'shared/runners/shared_runners_description' do
- - if !isVueifySharedRunnersToggleEnabled
- %br
- %br
- - if @project.group&.shared_runners_setting == 'disabled_and_unoverridable'
- %h5.gl-text-red-500
- = _('Shared runners disabled on group level')
- - else
- - if @project.shared_runners_enabled?
- = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn gl-button btn-warning-secondary', method: :post do
- = _('Disable shared runners')
- - else
- = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn gl-button btn-confirm', method: :post do
- = _('Enable shared runners')
- &nbsp; for this project
-
-- if isVueifySharedRunnersToggleEnabled
- #toggle-shared-runners-form{ data: toggle_shared_runners_settings_data(@project) }
+#toggle-shared-runners-form{ data: toggle_shared_runners_settings_data(@project) }
- if @shared_runners_count == 0
= _('This GitLab instance does not provide any shared runners yet. Instance administrators can register shared runners in the admin area.')
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 77150715158..e87c52ff1a8 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,4 +1,7 @@
-- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners')
+- breadcrumb_title _('Edit')
+- page_title _('Edit'), "##{@runner.id} (#{@runner.short_sha})"
+- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
+- add_to_breadcrumbs "#{@runner.short_sha}", project_runner_path(@project, @runner)
%h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
new file mode 100644
index 00000000000..cb7984729c8
--- /dev/null
+++ b/app/views/projects/runners/show.html.haml
@@ -0,0 +1,3 @@
+- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
+
+= render 'shared/runners/runner_details', runner: @runner
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
index 5e0f24cea21..be9bd3dfc01 100644
--- a/app/views/projects/settings/_archive.html.haml
+++ b/app/views/projects/settings/_archive.html.haml
@@ -14,7 +14,7 @@
method: :post, class: "gl-button btn btn-confirm"
- else
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') }
- %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
+ %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe }
= link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "gl-button btn btn-warning"
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index 01f3e441eef..1bf252b6282 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -9,13 +9,13 @@
%h4.gl-mt-0
= page_title
%p
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') }
- if current_user.can?(:create_resource_access_tokens, @project)
- = _('You can generate an access token scoped to this project for each application to use the GitLab API.')
- -# Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed
- -# %p
- -# = _('You can also use project access tokens to authenticate against Git over HTTP.')
+ = _('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.')
+ %p
+ = _('You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- else
- = _('Project access token creation is disabled in this group. You can still use and manage existing tokens.')
+ = _('Project access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
%p
- root_group = @project.group.root_ancestor
- if current_user.can?(:admin_group, root_group)
@@ -23,7 +23,6 @@
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link }
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
-
.col-lg-8
- if @new_project_access_token
= render 'shared/access_tokens/created_container',
diff --git a/app/views/projects/settings/operations/_configuration_banner.html.haml b/app/views/projects/settings/operations/_configuration_banner.html.haml
index 8551aa5380e..6fa6b23b0da 100644
--- a/app/views/projects/settings/operations/_configuration_banner.html.haml
+++ b/app/views/projects/settings/operations/_configuration_banner.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
%p.text-success.gl-mt-3
= s_('PrometheusService|GitLab manages Prometheus on your clusters.')
- = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
+ = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'gl-button btn btn-default'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 1e77f37ebb4..4ef9e1bd6fb 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -9,8 +9,8 @@
%button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
= _('Expand')
%p
- = _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.')
- = link_to _('More information'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
+ = _('Link Sentry to GitLab to discover and view the errors your application generates.')
+ = link_to _('Learn more.'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-error-tracking-form{ data: { list_projects_endpoint: project_error_tracking_projects_path(@project, format: :json),
operations_settings_endpoint: project_settings_operations_path(@project),
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 73722a5a789..af183046e1e 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -1,6 +1,7 @@
- @content_class = 'limit-container-width' unless fluid_layout
-- page_title _('Operations Settings')
-- breadcrumb_title _('Operations Settings')
+- title = Feature.enabled?(:sidebar_refactor, current_user, default_enabled: :yaml) ? _('Monitor Settings') : _('Operations Settings')
+- page_title title
+- breadcrumb_title title
= render 'projects/settings/operations/alert_management'
= render 'projects/settings/operations/incidents'
diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml
new file mode 100644
index 00000000000..561ac7b347d
--- /dev/null
+++ b/app/views/projects/settings/packages_and_registries/show.html.haml
@@ -0,0 +1,16 @@
+- breadcrumb_title _('Packages & Registries')
+- page_title _('Packages & Registries')
+- @content_class = 'limit-container-width' unless fluid_layout
+- expanded = true
+
+%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = _("Clean up image tags")
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.")
+ = link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render 'projects/registry/settings/index'
diff --git a/app/views/projects/sidebar/_issues_service_desk.html.haml b/app/views/projects/sidebar/_issues_service_desk.html.haml
deleted file mode 100644
index 2730fe37f28..00000000000
--- a/app/views/projects/sidebar/_issues_service_desk.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-= nav_link(controller: :issues, action: :service_desk ) do
- = link_to service_desk_project_issues_path(@project), title: 'Service Desk' do
- = _('Service Desk')
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 726ab7d2372..a296394a2e0 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -6,6 +6,6 @@
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
.row-content-block.top-block.content-component-block
- = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 1072d5bce06..83a3cac487f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -8,7 +8,7 @@
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name'
- if protected_tag?(@project, tag)
- %span.badge.badge-success.gl-ml-2
+ %span.badge.badge-success.gl-ml-2.gl-badge.sm.badge-pill
= s_('TagsPage|protected')
- if tag.message.present?
@@ -42,5 +42,5 @@
- if can?(current_user, :admin_tag, @project)
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
- = sprite_icon("pencil")
+ = sprite_icon('pencil', css_class: 'gl-icon')
= render 'projects/buttons/remove_tag', project: @project, tag: tag
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 229f13d0ff3..79205a51d71 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -9,28 +9,12 @@
= s_('TagsPage|Tags give the ability to mark specific points in history as being important')
.nav-controls
- - unless Gitlab::Ci::Features.gldropdown_tags_enabled?
- = form_tag(filter_tags_path, method: :get) do
- = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
-
- .dropdown
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
- %span.light
- = tags_sort_options_hash[@sort]
- = 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')
- - tags_sort_options_hash.each do |value, title|
- %li
- = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value)
- - else
- #js-tags-sort-dropdown{ data: { filter_tags_path: filter_tags_path, sort_options: tags_sort_options_hash.to_json } }
+ #js-tags-sort-dropdown{ data: { filter_tags_path: filter_tags_path, sort_options: tags_sort_options_hash.to_json } }
+ = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon has-tooltip gl-ml-auto' do
+ = sprite_icon('rss', css_class: 'gl-icon qa-rss-icon')
- if can?(current_user, :admin_tag, @project)
= link_to new_project_tag_path(@project), class: 'btn gl-button btn-confirm', data: { qa_selector: "new_tag_button" } do
= s_('TagsPage|New tag')
- = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-block has-tooltip' do
- = sprite_icon('rss', css_class: 'qa-rss-icon')
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 2ef1891089f..fe00772d1d6 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -51,7 +51,7 @@
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'shared/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description, qa_selector: 'release_notes_field'
= render 'shared/notes/hints'
- .form-actions
- = button_tag s_('TagsPage|Create tag'), class: 'gl-button btn btn-confirm', data: { qa_selector: "create_tag_button" }
+ .form-actions.gl-display-flex
+ = button_tag s_('TagsPage|Create tag'), class: 'gl-button btn btn-confirm gl-mr-3', data: { qa_selector: "create_tag_button" }
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'gl-button btn btn-default btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tags/releases/edit.html.haml b/app/views/projects/tags/releases/edit.html.haml
index f181212b328..88594209c3b 100644
--- a/app/views/projects/tags/releases/edit.html.haml
+++ b/app/views/projects/tags/releases/edit.html.haml
@@ -14,6 +14,6 @@
= render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
.error-alert
- .gl-mt-3
- = f.submit 'Save changes', class: 'btn gl-button btn-confirm'
+ .gl-mt-5.gl-display-flex
+ = f.submit 'Save changes', class: 'btn gl-button btn-confirm gl-mr-3'
= link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel"
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 2def6c06458..081afacdaa6 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -30,8 +30,8 @@
%td.text-right.trigger-actions
- revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
- if can?(current_user, :admin_trigger, trigger)
- = link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "gl-button btn btn-default btn-sm" do
+ = link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "gl-button btn btn-default btn-icon" do
= sprite_icon('pencil')
- if can?(current_user, :manage_trigger, trigger)
- = link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation, testid: 'trigger_revoke_button' }, method: :delete, title: "Revoke", class: "gl-button btn btn-default btn-sm btn-trigger-revoke" do
+ = link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation, testid: 'trigger_revoke_button' }, method: :delete, title: "Revoke", class: "gl-button btn btn-default btn-icon btn-trigger-revoke gl-ml-3" do
= sprite_icon('remove')
diff --git a/app/views/registrations/invites/new.html.haml b/app/views/registrations/invites/new.html.haml
new file mode 100644
index 00000000000..6e6ff7aaeee
--- /dev/null
+++ b/app/views/registrations/invites/new.html.haml
@@ -0,0 +1,18 @@
+- page_title _('Join your team')
+- add_page_specific_style 'page_bundles/signup'
+- content_for :page_specific_javascripts do
+ = render "layouts/google_tag_manager_head"
+= render "layouts/google_tag_manager_body"
+
+%h2.center.pt-6.pb-3.gl-mb-0
+ = _('Join your team')
+%p.gl-text-center= _('Create your own profile to collaborate with your teammates in issues, merge requests, and more.')
+
+.signup-page
+ = render 'devise/shared/signup_box',
+ url: users_sign_up_invites_path,
+ button_text: _('Continue'),
+ show_omniauth_providers: social_signin_enabled?,
+ omniauth_providers_placement: :top,
+ suggestion_path: nil
+ = render 'devise/shared/sign_in_link'
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index bf5e35a1224..e85ce1ba6ac 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -25,6 +25,7 @@
= f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/setup_for_company", f: f
= render 'devise/shared/email_opted_in', f: f
+ = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
.row
.form-group.col-sm-12.gl-mb-0
- if partial_exists? "registrations/welcome/button"
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index f094a6f5e3b..7f8a530deb8 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -13,23 +13,25 @@
= search_filter_link 'issues', _("Issues")
- if project_search_tabs?(:merge_requests)
= search_filter_link 'merge_requests', _("Merge requests")
- - if project_search_tabs?(:milestones)
- = search_filter_link 'milestones', _("Milestones")
- - if project_search_tabs?(:notes)
- = search_filter_link 'notes', _("Comments")
- if project_search_tabs?(:wiki)
= search_filter_link 'wiki_blobs', _("Wiki")
- if project_search_tabs?(:commits)
= search_filter_link 'commits', _("Commits")
+ - if project_search_tabs?(:notes)
+ = search_filter_link 'notes', _("Comments")
+ - if project_search_tabs?(:milestones)
+ = search_filter_link 'milestones', _("Milestones")
= users
- elsif @show_snippets
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
+ = render_if_exists 'search/category_code'
+ = render_if_exists 'search/epics_filter_link'
= search_filter_link 'issues', _("Issues")
= search_filter_link 'merge_requests', _("Merge requests")
- = search_filter_link 'milestones', _("Milestones")
- = render_if_exists 'search/epics_filter_link'
+ = render_if_exists 'search/category_wiki'
= render_if_exists 'search/category_elasticsearch'
+ = search_filter_link 'milestones', _("Milestones")
= users
diff --git a/app/views/search/results/_user.html.haml b/app/views/search/results/_user.html.haml
index 8060a1577e4..9e70d9c9baa 100644
--- a/app/views/search/results/_user.html.haml
+++ b/app/views/search/results/_user.html.haml
@@ -1,9 +1,9 @@
%ul.content-list
%li
- .avatar-cell.d-none.d-sm-block
- = user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40')
+ .avatar-cell
+ = user_avatar(user: user, size: 40, user_name: user.name)
.user-info
- = link_to user_path(user), class: 'd-none d-sm-inline' do
+ = link_to user_path(user) do
.item-title
= user.name
= user_status(user)
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index ca82f2f3377..93868f13e58 100644
--- a/app/views/shared/_allow_request_access.html.haml
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -1,6 +1,6 @@
- label_class = local_assigns.fetch(:bold_label, false) ? 'font-weight-bold' : ''
-.form-check
- = form.check_box :request_access_enabled, class: 'form-check-input', data: { qa_selector: 'request_access_checkbox' }
- = form.label :request_access_enabled, class: 'form-check-label' do
+.gl-form-checkbox.custom-control.custom-checkbox
+ = form.check_box :request_access_enabled, class: 'custom-control-input', data: { qa_selector: 'request_access_checkbox' }
+ = form.label :request_access_enabled, class: 'custom-control-label' do
%span{ class: label_class }= _('Allow users to request access (if visibility is public or internal)')
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 47ecc75af1f..904854c3fb7 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -7,11 +7,13 @@
.commit-message-container
.max-width-marker
= text_area_tag 'commit_message',
- (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
- class: 'form-control gl-form-input js-commit-message', placeholder: local_assigns[:placeholder],
- data: descriptions,
- required: true, rows: (local_assigns[:rows] || 3),
- id: "commit_message-#{nonce}"
+ (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
+ class: 'form-control gl-form-input js-commit-message',
+ placeholder: local_assigns[:placeholder],
+ data: descriptions,
+ 'data-qa-selector': 'commit_message_field',
+ required: true, rows: (local_assigns[:rows] || 3),
+ id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
= _('Try to keep the first line under 52 characters and the others under 72.')
diff --git a/app/views/shared/_confirm_fork_modal.html.haml b/app/views/shared/_confirm_fork_modal.html.haml
index 265396d3d8b..ed52aa01047 100644
--- a/app/views/shared/_confirm_fork_modal.html.haml
+++ b/app/views/shared/_confirm_fork_modal.html.haml
@@ -6,7 +6,7 @@
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body.p-3
- %p= _("You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.") % { tag_start: '', tag_end: ''}
+ %p= _("You can’t %{tag_start}edit%{tag_end} files directly in this project. Fork this project and submit a merge request with your changes.") % { tag_start: '', tag_end: ''}
.modal-footer
= link_to _('Cancel'), '#', class: "btn gl-button btn-default", "data-dismiss" => "modal"
= link_to _('Fork project'), fork_path, class: 'btn gl-button btn-confirm', data: { qa_selector: 'fork_project_button' }, method: :post
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index eea0c5f37de..7055dc8142a 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -6,7 +6,7 @@
.form-group.group-name-holder.col-sm-12
= f.label :name, class: 'label-bold' do
= _("Group name")
- = f.text_field :name, placeholder: _('My Awesome Group'), class: 'js-autofill-group-name form-control input-lg',
+ = f.text_field :name, placeholder: _('My Awesome Group'), class: 'js-autofill-group-name form-control input-lg', data: { qa_selector: 'group_name_field' },
required: true,
title: _('Please fill in a descriptive name for your group.'),
autofocus: true
@@ -22,7 +22,7 @@
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
- = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path',
+ = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path', data: { qa_selector: 'group_path_field' },
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: _('Please choose a group URL with no special characters.'),
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 36d8aab6d53..cf9ee1a5231 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -6,21 +6,27 @@
= f.label :import_url, class: 'label-bold' do
%span
= _('Git repository URL')
- = f.text_field :import_url, value: import_url.sanitized_url,
- autocomplete: 'off', class: 'form-control', placeholder: 'https://gitlab.company.com/group/project.git', required: true
+ = f.text_field :import_url,
+ value: import_url.sanitized_url,
+ autocomplete: 'off',
+ class: 'form-control gl-form-input',
+ placeholder: 'https://gitlab.company.com/group/project.git',
+ required: true,
+ pattern: '(?:git|https?):\/\/.*/.*\.git$',
+ title: _('Please provide a valid URL ending with .git')
.row
.form-group.col-md-6
= f.label :import_url_user, class: 'label-bold' do
%span
= _('Username (optional)')
- = f.text_field :import_url_user, value: import_url.user, class: 'form-control', required: false, autocomplete: 'new-password'
+ = f.text_field :import_url_user, value: import_url.user, class: 'form-control gl-form-input', required: false, autocomplete: 'new-password'
.form-group.col-md-6
= f.label :import_url_password, class: 'label-bold' do
%span
= _('Password (optional)')
- = f.password_field :import_url_password, class: 'form-control', required: false, autocomplete: 'new-password'
+ = f.password_field :import_url_password, class: 'form-control gl-form-input', required: false, autocomplete: 'new-password'
.info-well.prepend-top-20
.well-segment
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 6c3e15cbace..01ab7bf9cd4 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -6,7 +6,7 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests') }
+ %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests'), data: { testid: 'merge-requests' } }
= sprite_icon('merge-request', css_class: "gl-vertical-align-middle")
= issuable_mr
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index eb12e9d463c..6eb736b0710 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,5 +1,7 @@
+= render 'shared/alerts/positioning_disabled'
+
- if @issues.to_a.any?
- %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
+ %ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class, data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 2a2a1a911af..9c59d5ae1fa 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -18,7 +18,7 @@
%th= s_('AccessTokens|Created')
%th
= _('Last Used')
- = link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'token-activity'), target: '_blank'
+ = link_to sprite_icon('question-o'), help_page_path('user/profile/personal_access_tokens.md', anchor: 'view-the-last-time-a-token-was-used'), target: '_blank'
%th= _('Expires')
%th= _('Scopes')
%th
diff --git a/app/views/shared/alerts/_positioning_disabled.html.haml b/app/views/shared/alerts/_positioning_disabled.html.haml
new file mode 100644
index 00000000000..91c1d3463d8
--- /dev/null
+++ b/app/views/shared/alerts/_positioning_disabled.html.haml
@@ -0,0 +1,2 @@
+- if issue_repositioning_disabled?
+ = render 'shared/alert_info', body: _('Issues manual ordering is temporarily disabled for technical reasons.')
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 73f3d2a8fcd..033ed69da41 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -19,6 +19,9 @@
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a task list") })
+ = markdown_toolbar_button({ icon: "details-block",
+ data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" },
+ title: _("Add a collapsible section") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index bf70149812a..c1a50cfe718 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -7,6 +7,8 @@
- breadcrumb_title _("Epic Boards")
- else
- breadcrumb_title _("Issue Boards")
+ = render 'shared/alerts/positioning_disabled'
+
- page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards'
diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml
index c36f2c7c969..79817025565 100644
--- a/app/views/shared/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml
@@ -1,9 +1,10 @@
- dropdown_options = assignees_dropdown_options('issue')
+- relative_url = Gitlab.config.gitlab.relative_url_root || '/'
.block.assignee{ ref: "assigneeBlock" }
%template{ "v-if" => "issue.assignees" }
%sidebar-assignees-widget{ ":iid" => "String(issue.iid)",
- ":full-path" => "issue.path.split('/-/')[0].substring(1)",
+ ":full-path" => "issue.path.split('/-/')[0].substring(1).replace(`#{relative_url}`, '')",
":initial-assignees" => "issue.assignees",
- ":multiple-assignees" => "!Boolean(#{dropdown_options[:data][:"max-select"]})",
+ ":allow-multiple-assignees" => "!Boolean(#{dropdown_options[:data][:"max-select"]})",
"@assignees-updated" => "setAssignees" }
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 5c74e71b644..4973309edf5 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -2,23 +2,23 @@
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
- %span.badge.badge-pill.js-totalbuilds-count
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm.js-totalbuilds-count
= limited_counter_with_delimiter(all_builds)
%li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(all_builds.pending)
%li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(all_builds.running)
%li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
- %span.badge.badge-pill
+ %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
= limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index 37a56057268..452e54f9cd4 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -6,7 +6,7 @@
.form-group
= form.label :title, class: 'col-form-label col-sm-2'
- .col-sm-10= form.text_field :title, class: 'form-control gl-form-input', readonly: ('readonly' unless can?(current_user, :update_deploy_key, deploy_key))
+ .col-sm-10= form.text_field :title, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_key_title_field' }, readonly: ('readonly' unless can?(current_user, :update_deploy_key, deploy_key))
.form-group
- if deploy_key.new_record?
@@ -16,7 +16,7 @@
- link_start = "<a href='#{help_page_path('ssh/README')}' target='_blank' rel='noreferrer noopener'>".html_safe
- link_end = '</a>'
= _('Paste a public key here. %{link_start}How do I generate it?%{link_end}').html_safe % { link_start: link_start, link_end: link_end.html_safe }
- = form.text_area :key, class: 'form-control gl-form-input thin_area', rows: 5
+ = form.text_area :key, class: 'form-control gl-form-input thin_area', rows: 5, data: { qa_selector: 'deploy_key_field' }
- else
= form.label :fingerprint, class: 'col-form-label col-sm-2'
.col-sm-10
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
index be6fe94e497..388fe75e833 100644
--- a/app/views/shared/deploy_keys/_index.html.haml
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = expanded_by_default?
-%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings_content' } }
+%section.rspec-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Deploy keys')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
index 25357ccdc65..0c671b4a1c0 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -2,10 +2,10 @@
= form_errors(@deploy_keys.new_key)
.form-group.row
= f.label :title, class: "label-bold"
- = f.text_field :title, class: 'form-control gl-form-input', required: true
+ = f.text_field :title, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'deploy_key_title_field' }
.form-group.row
= f.label :key, class: "label-bold"
- = f.text_area :key, class: 'form-control gl-form-input', rows: 5, required: true
+ = f.text_area :key, class: 'form-control gl-form-input', rows: 5, required: true, data: { qa_selector: 'deploy_key_field' }
.form-group.row
%p.light.gl-mb-0
= _('Paste a public key here.')
@@ -21,4 +21,4 @@
= _('Allow this key to push to this repository')
.form-group.row
- = f.submit _("Add key"), class: "btn gl-button btn-confirm"
+ = f.submit _("Add key"), class: "btn gl-button btn-confirm", data: { qa_selector: "add_deploy_key_button"}
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 4d0858165a2..976776ccc62 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -6,28 +6,28 @@
.form-group
= f.label :name, class: 'label-bold'
- = f.text_field :name, class: 'form-control gl-form-input qa-deploy-token-name', required: true
+ = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
.form-group
= f.label :expires_at, _('Expires at (optional)'), class: 'label-bold'
- = f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at
+ = f.text_field :expires_at, class: 'datepicker form-control', data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
.text-secondary= s_('DeployTokens|Unless you enter a date, the token does not expire.')
.form-group
= f.label :username, _('Username (optional)'), class: 'label-bold'
- = f.text_field :username, class: 'form-control qa-deploy-token-username'
+ = f.text_field :username, class: 'form-control'
.text-secondary= s_('DeployTokens|Unless you specify a username, it is set to "gitlab+deploy-token-{n}".')
.form-group
= f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold'
%fieldset.form-group.form-check
- = f.check_box :read_repository, class: 'form-check-input qa-deploy-token-read-repository'
+ = f.check_box :read_repository, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_repository_checkbox' }
= f.label :read_repository, 'read_repository', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to the repository.')
- if container_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
- = f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry'
+ = f.check_box :read_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_registry_checkbox' }
= f.label :read_registry, 'read_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows read-only access to registry images.')
@@ -48,4 +48,4 @@
.text-secondary= s_('DeployTokens|Allows write access to the package registry.')
.gl-mt-3
- = f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm qa-create-deploy-token'
+ = f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' }
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index 9d1a24d4c24..3e8368b7b78 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -1,9 +1,9 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
-%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
+%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= s_('DeployTokens|Deploy tokens')
- %button.btn.gl-button.btn-default.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
+ %button.btn.gl-button.btn-default.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
= description
diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
index 41e50138220..9c82d5685f8 100644
--- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
+++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml
@@ -1,11 +1,11 @@
-.qa-created-deploy-token-section.created-deploy-token-container.info-well
+.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } }
.well-segment
%h5.gl-mt-0
= s_('DeployTokens|Your new Deploy Token username')
.form-group
.input-group
- = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token-user'
+ = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
.input-group-append
= clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
@@ -15,7 +15,7 @@
.form-group
.input-group
- = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token'
+ = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
.input-group-append
= clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 1e340f033a1..3e89969f46e 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -20,9 +20,11 @@
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
.check-all-holder.d-none.d-sm-block.hidden
- = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
+ - checkbox_id = 'check-all-issues'
+ %label.gl-sr-only{ for: checkbox_id }= _('Select all')
+ = check_box_tag checkbox_id, nil, false, class: "check-all-issues left"
- if Feature.enabled?(:boards_filtered_search, @group) && is_epic_board
- #js-board-filtered-search
+ #js-board-filtered-search{ data: { full_path: @group&.full_path } }
- else
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 47e7ff0e4bc..86369b32e98 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,10 @@
- issuable_type = issuable_sidebar[:type]
- dropdown_options = assignees_dropdown_options(issuable_type)
-#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in, max_assignees: dropdown_options[:data][:"max-select"], directly_invite_members: directly_invite_members?, indirectly_invite_members: indirectly_invite_members? } }
+#js-vue-sidebar-assignees{ data: { field: issuable_type,
+ signed_in: signed_in,
+ max_assignees: dropdown_options[:data][:"max-select"],
+ directly_invite_members: directly_invite_members? } }
.title.hide-collapsed
= _('Assignee')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
@@ -39,12 +42,12 @@
- data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- options[:data].merge!(data)
- - if directly_invite_members? || indirectly_invite_members?
+ - if directly_invite_members?
- options[:dropdown_class] += ' dropdown-extended-height'
- options[:footer_content] = true
- options[:wrapper_class] = 'js-sidebar-assignee-dropdown'
- options[:toggle_class] += ' js-invite-members-track'
- - data['track-event'] = show_invite_members_track_event
+ - data['track-event'] = 'show_invite_members'
- options[:data].merge!(data)
- invite_text = _('Invite Members')
- track_label = 'edit_assignee'
@@ -52,15 +55,9 @@
= dropdown_tag(title, options: options) do
%ul.dropdown-footer-list
%li
- - if directly_invite_members?
- .js-invite-members-trigger{ data: { trigger_element: 'anchor',
- display_text: invite_text,
- event: 'click_invite_members',
- label: track_label } }
- - else
- .js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
+ .js-invite-members-trigger{ data: { trigger_element: 'anchor',
+ display_text: invite_text,
+ event: 'click_invite_members',
+ label: track_label } }
- else
= dropdown_tag(title, options: options)
-
-- if indirectly_invite_members?
- .js-invite-member-modal{ data: { members_path: project_project_members_path(@project, sort: :access_level_desc) } }
diff --git a/app/views/shared/issuable/_status_box.html.haml b/app/views/shared/issuable/_status_box.html.haml
new file mode 100644
index 00000000000..c0e972684d2
--- /dev/null
+++ b/app/views/shared/issuable/_status_box.html.haml
@@ -0,0 +1,6 @@
+- state_human_name, state_icon_name = state_name_with_icon(issuable)
+
+.issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(issuable), data: { project_path: issuable.project.path_with_namespace, iid: issuable.iid, state: issuable.state } }
+ = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-sm-display-none!')
+ %span.gl-display-none.gl-sm-display-block
+ = state_human_name
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 94d0c395fa6..561ca0afd60 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -5,11 +5,8 @@
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>'
-- draft_snippet = '<code>Draft:</code>'.html_safe
-- wip_snippet = '<code>WIP:</code>'.html_safe
-- draft_or_wip_snippet = '<code>Draft/WIP</code>'.html_safe
-- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet} or %{wip_snippet}%{link_end} to prevent a merge request that is a work in progress from being merged before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: draft_snippet, wip_snippet: wip_snippet } ).html_safe
-- remove_wip_text = (_('%{link_start}Remove the %{draft_or_wip_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_or_wip_snippet: draft_or_wip_snippet } ).html_safe
+- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request that is a work in progress from being merged before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe } ).html_safe
+- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe } ).html_safe
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index 36a68dfdaa7..a25e35cdcd4 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -1,12 +1,17 @@
+- link = issue_closed_link(@issue, current_user, css_class: 'text-white text-underline')
+
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
= sprite_icon('issue-close', css_class: 'gl-display-block gl-sm-display-none!')
- .gl-display-none.gl-sm-display-block!
+ .gl-display-none.gl-sm-display-block
= issue_closed_text(issuable, current_user)
+ - if link
+ %span.text-white.gl-pl-2.gl-sm-display-none
+ = "(#{link})"
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }
= sprite_icon('issue-open-m', css_class: 'gl-display-block gl-sm-display-none!')
- %span.gl-display-none.gl-sm-display-block!
+ %span.gl-display-none.gl-sm-display-block
= _('Open')
.issuable-meta
diff --git a/app/views/shared/members/_invite_member.html.haml b/app/views/shared/members/_invite_member.html.haml
index ad0ba6dcedf..49111c821b1 100644
--- a/app/views/shared/members/_invite_member.html.haml
+++ b/app/views/shared/members/_invite_member.html.haml
@@ -23,6 +23,6 @@
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
= sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200')
- = submit_tag _("Invite"), class: "gl-button btn btn-confirm gl-mr-3", data: { qa_selector: 'invite_member_button' }
+ = submit_tag _("Invite"), class: "gl-button btn btn-confirm gl-mr-2", data: { qa_selector: 'invite_member_button' }
- if can_import_members
= link_to _("Import"), import_path, class: "gl-button btn btn-default", title: _("Import members from another project")
diff --git a/app/views/shared/milestones/_delete_button.html.haml b/app/views/shared/milestones/_delete_button.html.haml
index 09c783a0b24..6d4ff255f06 100644
--- a/app/views/shared/milestones/_delete_button.html.haml
+++ b/app/views/shared/milestones/_delete_button.html.haml
@@ -7,6 +7,6 @@
milestone_merge_request_count: @milestone.merge_requests.count },
disabled: true }
= _('Delete')
- .spinner.js-loading-icon.hidden
+ .gl-spinner.js-loading-icon.hidden
#js-delete-milestone-modal
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index e995584309a..e0664c1feba 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -3,11 +3,11 @@
.col-form-label.col-sm-2
= f.label :start_date, _('Start Date')
.col-sm-10
- = f.text_field :start_date, class: "datepicker form-control", data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off'
+ = f.text_field :start_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "start_date_field" }, placeholder: _('Select start date'), autocomplete: 'off'
%a.inline.float-right.gl-mt-2.js-clear-start-date{ href: "#" }= _('Clear start date')
.form-group.row
.col-form-label.col-sm-2
= f.label :due_date, _('Due Date')
.col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off'
+ = f.text_field :due_date, class: "datepicker form-control gl-form-input", data: { qa_selector: "due_date_field" }, placeholder: _('Select due date'), autocomplete: 'off'
%a.inline.float-right.gl-mt-2.js-clear-due-date{ href: "#" }= _('Clear due date')
diff --git a/app/views/shared/milestones/_search_form.html.haml b/app/views/shared/milestones/_search_form.html.haml
index 403a0224a85..1c51f1ad09d 100644
--- a/app/views/shared/milestones/_search_form.html.haml
+++ b/app/views/shared/milestones/_search_form.html.haml
@@ -1,7 +1,7 @@
= form_tag request.path, method: :get do |f|
= search_field_tag :search_title, params[:search_title],
placeholder: _('Filter by milestone name'),
- class: 'form-control input-short',
+ class: 'form-control gl-form-input input-short',
spellcheck: false
= hidden_field_tag :state, params[:state]
= hidden_field_tag :sort, params[:sort]
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 0e54f1a7672..0088cd35781 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -79,7 +79,7 @@
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
= s_('MilestoneSidebar|Issues')
- %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count
+ %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.issues_visible_to_user(current_user).count
- if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: s_('MilestoneSidebar|New Issue') do
= s_('MilestoneSidebar|New issue')
@@ -110,7 +110,7 @@
%span= milestone.merge_requests.count
.title.hide-collapsed
= s_('MilestoneSidebar|Merge requests')
- %span.badge.badge-pill= milestone.merge_requests.count
+ %span.badge.badge-muted.badge-pill.gl-badge.sm= milestone.merge_requests.count
.value.hide-collapsed.bold
- if !project || can?(current_user, :read_merge_request, project)
%span.milestone-stat
diff --git a/app/views/shared/milestones/_tab_loading.html.haml b/app/views/shared/milestones/_tab_loading.html.haml
index fe1184114e9..b19e994ef80 100644
--- a/app/views/shared/milestones/_tab_loading.html.haml
+++ b/app/views/shared/milestones/_tab_loading.html.haml
@@ -1,2 +1,2 @@
.text-center.gl-mt-3
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
index 1e9aa4ec5ff..ab4d8816ec9 100644
--- a/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
+++ b/app/views/shared/namespaces/cascading_settings/_enforcement_checkbox.html.haml
@@ -1,14 +1,17 @@
- attribute = local_assigns.fetch(:attribute, nil)
-- group = local_assigns.fetch(:group, nil)
- form = local_assigns.fetch(:form, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, s_('CascadingSettings|Subgroups cannot change this setting.'))
- return unless attribute && group && form && cascading_namespace_settings_enabled?
-- return if group.namespace_settings.public_send("#{attribute}_locked?")
+- return if setting_locked
- lock_attribute = "lock_#{attribute}"
.gl-form-checkbox.custom-control.custom-checkbox
= form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
= form.label lock_attribute, class: 'custom-control-label' do
- %span= s_('CascadingSettings|Enforce for all subgroups')
- %p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.')
+ %span
+ = yield.presence || s_('CascadingSettings|Enforce for all subgroups')
+ %p.help-text
+ = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
new file mode 100644
index 00000000000..4e3b6b2afc4
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_lock_icon.html.haml
@@ -0,0 +1,4 @@
+%button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
+ type: 'button',
+ data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
+ = sprite_icon('lock', size: 16)
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
deleted file mode 100644
index 6596ce2bc73..00000000000
--- a/app/views/shared/namespaces/cascading_settings/_setting_label.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- attribute = local_assigns.fetch(:attribute, nil)
-- group = local_assigns.fetch(:group, nil)
-- form = local_assigns.fetch(:form, nil)
-- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
-- help_text = local_assigns.fetch(:help_text, nil)
-
-- return unless attribute && group && form && settings_path_helper
-
-- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?")
-
-= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
- %span.position-relative.gl-pr-6.gl-display-inline-flex
- = yield
- - if setting_locked
- %button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
- type: 'button',
- data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
- = sprite_icon('lock', size: 16)
- - if help_text
- %p.help-text
- = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml
new file mode 100644
index 00000000000..d27b3641637
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_checkbox.html.haml
@@ -0,0 +1,16 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
+- form = local_assigns.fetch(:form, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, nil)
+
+- return unless attribute && form && settings_path_helper
+
+= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
+ = render 'shared/namespaces/cascading_settings/setting_label_container' do
+ = yield
+ - if setting_locked
+ = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns
+ - if help_text
+ %p.help-text
+ = help_text
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml
new file mode 100644
index 00000000000..7323295f1fe
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_container.html.haml
@@ -0,0 +1,2 @@
+%span.position-relative.gl-pr-6.gl-display-inline-flex
+ = yield
diff --git a/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml b/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml
new file mode 100644
index 00000000000..4a2ec9f30fd
--- /dev/null
+++ b/app/views/shared/namespaces/cascading_settings/_setting_label_fieldset.html.haml
@@ -0,0 +1,15 @@
+- attribute = local_assigns.fetch(:attribute, nil)
+- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
+- setting_locked = local_assigns.fetch(:setting_locked, false)
+- help_text = local_assigns.fetch(:help_text, nil)
+
+- return unless attribute && settings_path_helper
+
+%legend.h5.gl-border-none.gl-m-0
+ = render 'shared/namespaces/cascading_settings/setting_label_container' do
+ = yield
+ - if setting_locked
+ = render 'shared/namespaces/cascading_settings/lock_icon', local_assigns
+- if help_text
+ %p.gl-text-gray-500
+ = help_text
diff --git a/app/views/shared/nav/_scope_menu.html.haml b/app/views/shared/nav/_scope_menu.html.haml
index 270587f48a8..2f10914ef3d 100644
--- a/app/views/shared/nav/_scope_menu.html.haml
+++ b/app/views/shared/nav/_scope_menu.html.haml
@@ -1,6 +1,6 @@
.context-header
= link_to scope_menu.link, **scope_menu.container_html_options do
- .avatar-container.rect-avatar.s40.project-avatar
+ %span.avatar-container.rect-avatar.s40.project-avatar
= source_icon(scope_menu.container, alt: scope_menu.title, class: 'avatar s40 avatar-tile', width: 40, height: 40)
- .sidebar-context-title
+ %span.sidebar-context-title
= scope_menu.title
diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml
index 1c06fc9eebf..552dcbfd6fd 100644
--- a/app/views/shared/nav/_sidebar.html.haml
+++ b/app/views/shared/nav/_sidebar.html.haml
@@ -11,4 +11,5 @@
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial
+ = render partial: 'shared/nav/sidebar_hidden_menu_item', collection: sidebar.hidden_menu&.renderable_items
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml
new file mode 100644
index 00000000000..953f7a8ae60
--- /dev/null
+++ b/app/views/shared/nav/_sidebar_hidden_menu_item.html.haml
@@ -0,0 +1,3 @@
+%li.hidden
+ = link_to sidebar_hidden_menu_item.link, **sidebar_hidden_menu_item.container_html_options do
+ = sidebar_hidden_menu_item.title
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
index c6e86a90ba7..67c775d1a85 100644
--- a/app/views/shared/nav/_sidebar_menu.html.haml
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -1,7 +1,7 @@
= nav_link(**sidebar_menu.all_active_routes, html_options: sidebar_menu.nav_link_html_options) do
= link_to sidebar_menu.link, **sidebar_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: sidebar_menu.title } do
- if sidebar_menu.icon_or_image?
- .nav-icon-container
+ %span.nav-icon-container
- if sidebar_menu.image_path
= image_tag(sidebar_menu.image_path, **sidebar_menu.image_html_options)
- elsif sidebar_menu.sprite_icon
@@ -13,15 +13,15 @@
%span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
= number_with_delimiter(sidebar_menu.pill_count)
- %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_items?) }
+ %ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
= nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
- = link_to sidebar_menu.link, aria: { label: sidebar_menu.title } do
+ = link_to sidebar_menu.link, **sidebar_menu.collapsed_container_html_options do
%strong.fly-out-top-item-name
= sidebar_menu.title
- if sidebar_menu.has_pill?
%span.badge.badge-pill.count.fly-out-badge{ **sidebar_menu.pill_html_options }
= number_with_delimiter(sidebar_menu.pill_count)
- - if sidebar_menu.has_renderable_items?
+ - if sidebar_menu.has_items?
%li.divider.fly-out-top-item
= render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/_runner_details.html.haml
index 757ec870f79..672f0b6a83f 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/_runner_details.html.haml
@@ -1,8 +1,9 @@
-- page_title "#{@runner.description} ##{@runner.id}", _("Runners")
+- breadcrumb_title runner.short_sha
+- page_title "##{runner.id} (#{runner.short_sha})"
%h2.page-title
- = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
- = render 'shared/runners/runner_type_badge', runner: @runner
+ = s_('Runners|Runner #%{runner_id}' % { runner_id: runner.id })
+ = render 'shared/runners/runner_type_badge', runner: runner
.table-holder
%table.table
@@ -12,51 +13,51 @@
%th= s_('Runners|Value')
%tr
%td= s_('Runners|Active')
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= runner.active? ? _('Yes') : _('No')
%tr
%td= s_('Runners|Protected')
- %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %td= runner.ref_protected? ? _('Yes') : _('No')
%tr
%td= s_('Runners|Can run untagged jobs')
- %td= @runner.run_untagged? ? _('Yes') : _('No')
- - unless @runner.group_type?
+ %td= runner.run_untagged? ? _('Yes') : _('No')
+ - unless runner.group_type?
%tr
%td= s_('Runners|Locked to this project')
- %td= @runner.locked? ? _('Yes') : _('No')
+ %td= runner.locked? ? _('Yes') : _('No')
%tr
%td= s_('Runners|Tags')
%td
- - @runner.tag_list.sort.each do |tag|
+ - runner.tag_list.sort.each do |tag|
%span.badge.badge-primary
= tag
%tr
%td= s_('Runners|Name')
- %td= @runner.name
+ %td= runner.name
%tr
%td= s_('Runners|Version')
- %td= @runner.version
+ %td= runner.version
%tr
%td= s_('Runners|IP Address')
- %td= @runner.ip_address
+ %td= runner.ip_address
%tr
%td= s_('Runners|Revision')
- %td= @runner.revision
+ %td= runner.revision
%tr
%td= s_('Runners|Platform')
- %td= @runner.platform
+ %td= runner.platform
%tr
%td= s_('Runners|Architecture')
- %td= @runner.architecture
+ %td= runner.architecture
%tr
%td= s_('Runners|Description')
- %td= @runner.description
+ %td= runner.description
%tr
%td= s_('Runners|Maximum job timeout')
- %td= @runner.maximum_timeout_human_readable
+ %td= runner.maximum_timeout_human_readable
%tr
%td= s_('Runners|Last contact')
%td
- - if @runner.contacted_at
- = time_ago_with_tooltip @runner.contacted_at
+ - if runner.contacted_at
+ = time_ago_with_tooltip runner.contacted_at
- else
= s_('Never')
diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml
index 92564ec48bd..a276f725576 100644
--- a/app/views/shared/runners/_shared_runners_description.html.haml
+++ b/app/views/shared/runners/_shared_runners_description.html.haml
@@ -3,11 +3,9 @@
%h4
= _('Shared runners')
-.bs-callout.shared-runners-description
- = _('These runners are shared across this GitLab instance.')
- %p
+.bs-callout{ data: { testid: 'shared-runners-description' } }
+ %p= _('These runners are shared across this GitLab instance.')
- if Gitlab::CurrentSettings.shared_runners_text.present?
- = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text)
+ = markdown(Gitlab::CurrentSettings.current_application_settings.shared_runners_text)
- else
- = _('The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com).').html_safe % { link: link }
- = yield
+ %p= _('The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com).').html_safe % { link: link }
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 52cf0248f21..4e373dda013 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -8,7 +8,7 @@
= link_to gitlab_snippet_path(snippet) do
= snippet.title
- %ul.controls
+ %ul.controls{ data: { qa_selector: 'snippet_file_count_content', qa_snippet_files: snippet.statistics&.file_count } }
%li
= snippet_file_count(snippet)
%li
@@ -16,7 +16,7 @@
= sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= notes_count
%li
- %span.sr-only
+ %span.sr-only{ data: { qa_selector: 'snippet_visibility_content', qa_snippet_visibility: visibility_level_label(snippet.visibility_level) } }
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level)
diff --git a/app/views/shared/ssh_keys/_key_delete.html.haml b/app/views/shared/ssh_keys/_key_delete.html.haml
index 1526e5d3eda..f8bb0e21f67 100644
--- a/app/views/shared/ssh_keys/_key_delete.html.haml
+++ b/app/views/shared/ssh_keys/_key_delete.html.haml
@@ -1,6 +1,9 @@
+- title = _('Delete Key')
+- aria = { label: title }
+
- if defined?(text)
- = button_to text, '#', class: html_class, data: button_data
+ = button_to text, '#', class: html_class, data: button_data, title: title, aria: aria
- else
- = button_to '#', class: html_class, data: button_data do
+ = button_to '#', class: html_class, data: button_data, title: title, aria: aria do
%span.sr-only= _('Delete')
= sprite_icon('remove')
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 1c8e300fa8a..33e95446bd7 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -4,6 +4,6 @@
- scopes.each do |scope|
%fieldset.form-group.form-check
- = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio"
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input", data: { qa_selector: "#{scope}_checkbox" }
= label_tag "#{prefix}_scopes_#{scope}", scope, class: 'label-bold form-check-label'
.text-secondary= t scope, scope: scope_description(prefix)
diff --git a/app/views/shared/users/_user.html.haml b/app/views/shared/users/_user.html.haml
index f92c12102bb..7f7cd31591e 100644
--- a/app/views/shared/users/_user.html.haml
+++ b/app/views/shared/users/_user.html.haml
@@ -7,7 +7,7 @@
.user-info
.block-truncated
- = link_to user.name, user_path(user), class: 'user js-user-link', data: { user_id: user.id }
+ = link_to user.name, user_path(user), class: 'user js-user-link', data: { user_id: user.id, qa_selector: 'user_link', qa_username: user.username }
.block-truncated
%span.gl-text-gray-900= user.to_reference
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ad84ce1d343..18912bf149f 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -43,13 +43,13 @@
= form.label :issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Issues events')
%p.text-muted.gl-ml-1
- = s_('Webhooks|URL is triggered when an issue is created, updated, or merged')
+ = s_('Webhooks|URL is triggered when an issue is created, updated, closed, or reopened')
%li
= form.check_box :confidential_issues_events, class: 'form-check-input'
= form.label :confidential_issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Confidential issues events')
%p.text-muted.gl-ml-1
- = s_('Webhooks|URL is triggered when a confidential issue is created, updated, or merged')
+ = s_('Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened')
- if @group
= render_if_exists 'groups/hooks/member_events', form: form
= render_if_exists 'groups/hooks/subgroup_events', form: form
diff --git a/app/views/shared/wikis/history.html.haml b/app/views/shared/wikis/history.html.haml
index 079b9768730..afbed3b0f42 100644
--- a/app/views/shared/wikis/history.html.haml
+++ b/app/views/shared/wikis/history.html.haml
@@ -20,7 +20,7 @@
%th= _('Changes')
%th= _('Last updated')
%tbody
- - @page_versions.each do |commit|
+ - @commits.each do |commit|
%tr
%td
= link_to wiki_page_path(@wiki, @page, version_id: commit.id) do
@@ -33,6 +33,6 @@
= commit.message
%td
= time_ago_with_tooltip(commit.authored_date)
- = paginate @page_versions, theme: 'gitlab'
+ = paginate @commits, theme: 'gitlab'
= render 'shared/wikis/sidebar'
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index e4a48943115..eea13d105d8 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -11,7 +11,7 @@
.row-content-block
.float-right
- = link_to(sherlock_transaction_path(@transaction), class: 'btn') do
+ = link_to(sherlock_transaction_path(@transaction), class: 'btn gl-button btn-default') do
= sprite_icon('arrow-left')
= t('sherlock.transaction')
.oneline
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index e9c9ca6e856..ac6dac8b322 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -5,7 +5,7 @@
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
= _("All")
- %span.badge.badge-pill
+ %span.badge.badge-muted.badge-pill.gl-badge.sm
- if include_private
= counts[:total]
- else
@@ -15,17 +15,17 @@
%li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
= _("Private")
- %span.badge.badge-pill
+ %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_private]
%li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
= _("Internal")
- %span.badge.badge-pill
+ %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_internal]
%li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
= _("Public")
- %span.badge.badge-pill
+ %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_public]
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 66f5e8148e1..f737e347c39 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,5 +1,7 @@
- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
- @content_class = "limit-container-width" unless fluid_layout
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco')
%h3.page-title
= _("Edit Snippet")
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index beb4cf4a6aa..4fdb9e70742 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -9,6 +9,8 @@
- add_to_breadcrumbs _("Snippets"), dashboard_snippets_path
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
+- content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco', prefetch: true)
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml
index a78971967ff..2e6d335a98d 100644
--- a/app/views/users/_overview.html.haml
+++ b/app/views/users/_overview.html.haml
@@ -3,7 +3,7 @@
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
.user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } }
- .spinner.spinner-md.gl-my-8
+ .gl-spinner.gl-spinner-md.gl-my-8
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')
%a.js-retry-load{ href: '#' }
@@ -18,9 +18,9 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- .overview-content-list{ data: { href: user_activity_path } }
+ .overview-content-list{ data: { href: user_activity_path, qa_selector: 'user_activity_content' } }
.center.light.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
.col-md-12.col-lg-6
@@ -32,4 +32,4 @@
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index daa41e0ebfe..a5b95883361 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -39,7 +39,7 @@
= link_to user_unfollow_path(@user, :json) , class: link_classes + 'btn gl-button btn-default', method: :post do
= _('Unfollow')
- else
- = link_to user_follow_path(@user, :json) , class: link_classes + 'btn gl-button btn-confirm', method: :post do
+ = link_to user_follow_path(@user, :json) , class: link_classes + 'btn gl-button btn-confirm', method: :post, data: { qa_selector: 'follow_user_link' } do
= _('Follow')
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
@@ -103,7 +103,7 @@
- count = @user.followers.count
= n_('1 follower', '%{count} followers', count) % { count: count }
.profile-link-holder.middle-dot-divider
- = link_to user_following_path, class: 'text-link' do
+ = link_to user_following_path, class: 'text-link', data: { qa_selector: 'following_link' } do
= @user.followees.count
= _('following')
- if @user.bio.present?
@@ -169,7 +169,7 @@
= s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_activity_path } }
.loading
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
@@ -200,7 +200,7 @@
-# This tab is always loaded via AJAX
.loading.hide
- .spinner.spinner-md
+ .gl-spinner.gl-spinner-md
- if profile_tabs.empty?
.svg-content
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index 8d589c03259..ea7709c649f 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -2,6 +2,8 @@
class AdminEmailWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index fa6ea54e342..07c1ce0d939 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -4,6 +4,7 @@
# Do not edit it manually!
---
- :name: authorized_project_update:authorized_project_update_project_create
+ :worker_name: AuthorizedProjectUpdate::ProjectCreateWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -12,6 +13,7 @@
:idempotent: true
:tags: []
- :name: authorized_project_update:authorized_project_update_project_group_link_create
+ :worker_name: AuthorizedProjectUpdate::ProjectGroupLinkCreateWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -20,6 +22,7 @@
:idempotent: true
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range
+ :worker_name: AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -28,6 +31,7 @@
:idempotent:
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
+ :worker_name: AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -36,6 +40,7 @@
:idempotent: true
:tags: []
- :name: auto_devops:auto_devops_disable
+ :worker_name: AutoDevops::DisableWorker
:feature_category: :auto_devops
:has_external_dependencies:
:urgency: :low
@@ -44,6 +49,7 @@
:idempotent:
:tags: []
- :name: auto_merge:auto_merge_process
+ :worker_name: AutoMergeProcessWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -52,46 +58,57 @@
:idempotent:
:tags: []
- :name: chaos:chaos_cpu_spin
+ :worker_name: Chaos::CpuSpinWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: chaos:chaos_db_spin
+ :worker_name: Chaos::DbSpinWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: chaos:chaos_kill
+ :worker_name: Chaos::KillWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: chaos:chaos_leak_mem
+ :worker_name: Chaos::LeakMemWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: chaos:chaos_sleep
+ :worker_name: Chaos::SleepWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: container_repository:cleanup_container_repository
+ :worker_name: CleanupContainerRepositoryWorker
:feature_category: :container_registry
:has_external_dependencies:
:urgency: :low
@@ -100,14 +117,17 @@
:idempotent: true
:tags: []
- :name: container_repository:container_expiration_policies_cleanup_container_repository
+ :worker_name: ContainerExpirationPolicies::CleanupContainerRepositoryWorker
:feature_category: :container_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: container_repository:delete_container_repository
+ :worker_name: DeleteContainerRepositoryWorker
:feature_category: :container_registry
:has_external_dependencies:
:urgency: :low
@@ -116,6 +136,7 @@
:idempotent:
:tags: []
- :name: cronjob:admin_email
+ :worker_name: AdminEmailWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -124,22 +145,27 @@
:idempotent:
:tags: []
- :name: cronjob:analytics_instance_statistics_count_job_trigger
+ :worker_name: Analytics::InstanceStatistics::CountJobTriggerWorker
:feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:analytics_usage_trends_count_job_trigger
+ :worker_name: Analytics::UsageTrends::CountJobTriggerWorker
:feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:authorized_project_update_periodic_recalculate
+ :worker_name: AuthorizedProjectUpdate::PeriodicRecalculateWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -148,6 +174,7 @@
:idempotent: true
:tags: []
- :name: cronjob:ci_archive_traces_cron
+ :worker_name: Ci::ArchiveTracesCronWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -155,7 +182,8 @@
:weight: 1
:idempotent:
:tags: []
-- :name: cronjob:ci_pipeline_artifacts_expire_artifacts
+- :name: cronjob:ci_delete_unit_tests
+ :worker_name: Ci::DeleteUnitTestsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -163,7 +191,18 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:ci_pipeline_artifacts_expire_artifacts
+ :worker_name: Ci::PipelineArtifacts::ExpireArtifactsWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:ci_platform_metrics_update_cron
+ :worker_name: CiPlatformMetricsUpdateCronWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -172,14 +211,17 @@
:idempotent:
:tags: []
- :name: cronjob:ci_schedule_delete_objects_cron
+ :worker_name: Ci::ScheduleDeleteObjectsCronWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:container_expiration_policy
+ :worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
:has_external_dependencies:
:urgency: :low
@@ -188,14 +230,17 @@
:idempotent:
:tags: []
- :name: cronjob:database_batched_background_migration
+ :worker_name: Database::BatchedBackgroundMigrationWorker
:feature_category: :database
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:environments_auto_stop_cron
+ :worker_name: Environments::AutoStopCronWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -204,6 +249,7 @@
:idempotent:
:tags: []
- :name: cronjob:expire_build_artifacts
+ :worker_name: ExpireBuildArtifactsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -212,6 +258,7 @@
:idempotent:
:tags: []
- :name: cronjob:gitlab_usage_ping
+ :worker_name: GitlabUsagePingWorker
:feature_category: :usage_ping
:has_external_dependencies:
:urgency: :low
@@ -220,6 +267,7 @@
:idempotent:
:tags: []
- :name: cronjob:import_export_project_cleanup
+ :worker_name: ImportExportProjectCleanupWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -228,6 +276,7 @@
:idempotent:
:tags: []
- :name: cronjob:import_stuck_project_import_jobs
+ :worker_name: Gitlab::Import::StuckProjectImportJobsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -236,6 +285,7 @@
:idempotent:
:tags: []
- :name: cronjob:issue_due_scheduler
+ :worker_name: IssueDueSchedulerWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -244,6 +294,7 @@
:idempotent:
:tags: []
- :name: cronjob:jira_import_stuck_jira_import_jobs
+ :worker_name: Gitlab::JiraImport::StuckJiraImportJobsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -252,14 +303,17 @@
:idempotent:
:tags: []
- :name: cronjob:member_invitation_reminder_emails
+ :worker_name: MemberInvitationReminderEmailsWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
+ :worker_name: Metrics::Dashboard::ScheduleAnnotationsPruneWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
@@ -268,14 +322,17 @@
:idempotent: true
:tags: []
- :name: cronjob:namespaces_in_product_marketing_emails
+ :worker_name: Namespaces::InProductMarketingEmailsWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:namespaces_prune_aggregation_schedules
+ :worker_name: Namespaces::PruneAggregationSchedulesWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -284,14 +341,17 @@
:idempotent:
:tags: []
- :name: cronjob:packages_composer_cache_cleanup
+ :worker_name: Packages::Composer::CacheCleanupWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:pages_domain_removal_cron
+ :worker_name: PagesDomainRemovalCronWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -300,6 +360,7 @@
:idempotent:
:tags: []
- :name: cronjob:pages_domain_ssl_renewal_cron
+ :worker_name: PagesDomainSslRenewalCronWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -308,6 +369,7 @@
:idempotent:
:tags: []
- :name: cronjob:pages_domain_verification_cron
+ :worker_name: PagesDomainVerificationCronWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -316,6 +378,7 @@
:idempotent:
:tags: []
- :name: cronjob:partition_creation
+ :worker_name: PartitionCreationWorker
:feature_category: :database
:has_external_dependencies:
:urgency: :low
@@ -324,14 +387,17 @@
:idempotent: true
:tags: []
- :name: cronjob:personal_access_tokens_expired_notification
+ :worker_name: PersonalAccessTokens::ExpiredNotificationWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:personal_access_tokens_expiring
+ :worker_name: PersonalAccessTokens::ExpiringWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -340,6 +406,7 @@
:idempotent:
:tags: []
- :name: cronjob:pipeline_schedule
+ :worker_name: PipelineScheduleWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -348,6 +415,7 @@
:idempotent:
:tags: []
- :name: cronjob:prune_old_events
+ :worker_name: PruneOldEventsWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
@@ -356,6 +424,7 @@
:idempotent:
:tags: []
- :name: cronjob:prune_web_hook_logs
+ :worker_name: PruneWebHookLogsWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
@@ -364,14 +433,17 @@
:idempotent:
:tags: []
- :name: cronjob:releases_manage_evidence
+ :worker_name: Releases::ManageEvidenceWorker
:feature_category: :release_evidence
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:remove_expired_group_links
+ :worker_name: RemoveExpiredGroupLinksWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -380,6 +452,7 @@
:idempotent:
:tags: []
- :name: cronjob:remove_expired_members
+ :worker_name: RemoveExpiredMembersWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -388,14 +461,17 @@
:idempotent:
:tags: []
- :name: cronjob:remove_unaccepted_member_invites
+ :worker_name: RemoveUnacceptedMemberInvitesWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:remove_unreferenced_lfs_objects
+ :worker_name: RemoveUnreferencedLfsObjectsWorker
:feature_category: :git_lfs
:has_external_dependencies:
:urgency: :low
@@ -404,6 +480,7 @@
:idempotent:
:tags: []
- :name: cronjob:repository_archive_cache
+ :worker_name: RepositoryArchiveCacheWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -412,6 +489,7 @@
:idempotent:
:tags: []
- :name: cronjob:repository_check_dispatch
+ :worker_name: RepositoryCheck::DispatchWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -420,6 +498,7 @@
:idempotent:
:tags: []
- :name: cronjob:requests_profiles
+ :worker_name: RequestsProfilesWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -428,14 +507,17 @@
:idempotent:
:tags: []
- :name: cronjob:schedule_merge_request_cleanup_refs
+ :worker_name: ScheduleMergeRequestCleanupRefsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:schedule_migrate_external_diffs
+ :worker_name: ScheduleMigrateExternalDiffsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -444,22 +526,27 @@
:idempotent:
:tags: []
- :name: cronjob:ssh_keys_expired_notification
+ :worker_name: SshKeys::ExpiredNotificationWorker
:feature_category: :compliance_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:ssh_keys_expiring_soon_notification
+ :worker_name: SshKeys::ExpiringSoonNotificationWorker
:feature_category: :compliance_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:stuck_ci_jobs
+ :worker_name: StuckCiJobsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -468,6 +555,7 @@
:idempotent:
:tags: []
- :name: cronjob:stuck_export_jobs
+ :worker_name: StuckExportJobsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -476,6 +564,7 @@
:idempotent:
:tags: []
- :name: cronjob:stuck_merge_jobs
+ :worker_name: StuckMergeJobsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -484,6 +573,7 @@
:idempotent:
:tags: []
- :name: cronjob:trending_projects
+ :worker_name: TrendingProjectsWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -492,6 +582,7 @@
:idempotent:
:tags: []
- :name: cronjob:update_container_registry_info
+ :worker_name: UpdateContainerRegistryInfoWorker
:feature_category: :container_registry
:has_external_dependencies:
:urgency: :low
@@ -500,14 +591,17 @@
:idempotent: true
:tags: []
- :name: cronjob:user_status_cleanup_batch
+ :worker_name: UserStatusCleanup::BatchWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:users_create_statistics
+ :worker_name: Users::CreateStatisticsWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
@@ -515,7 +609,18 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:users_deactivate_dormant_users
+ :worker_name: Users::DeactivateDormantUsersWorker
+ :feature_category: :utilization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags:
+ - :exclude_from_kubernetes
- :name: cronjob:x509_issuer_crl_check
+ :worker_name: X509IssuerCrlCheckWorker
:feature_category: :source_code_management
:has_external_dependencies: true
:urgency: :low
@@ -524,6 +629,7 @@
:idempotent: true
:tags: []
- :name: dependency_proxy:purge_dependency_proxy_cache
+ :worker_name: PurgeDependencyProxyCacheWorker
:feature_category: :dependency_proxy
:has_external_dependencies:
:urgency: :low
@@ -532,14 +638,17 @@
:idempotent: true
:tags: []
- :name: deployment:deployments_drop_older_deployments
+ :worker_name: Deployments::DropOlderDeploymentsWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: deployment:deployments_execute_hooks
+ :worker_name: Deployments::ExecuteHooksWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -548,6 +657,7 @@
:idempotent:
:tags: []
- :name: deployment:deployments_finished
+ :worker_name: Deployments::FinishedWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -556,6 +666,16 @@
:idempotent:
:tags: []
- :name: deployment:deployments_forward_deployment
+ :worker_name: Deployments::ForwardDeploymentWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 3
+ :idempotent:
+ :tags: []
+- :name: deployment:deployments_hooks
+ :worker_name: Deployments::HooksWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -564,6 +684,7 @@
:idempotent:
:tags: []
- :name: deployment:deployments_link_merge_request
+ :worker_name: Deployments::LinkMergeRequestWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -572,6 +693,7 @@
:idempotent: true
:tags: []
- :name: deployment:deployments_success
+ :worker_name: Deployments::SuccessWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -580,6 +702,7 @@
:idempotent:
:tags: []
- :name: deployment:deployments_update_environment
+ :worker_name: Deployments::UpdateEnvironmentWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -588,6 +711,7 @@
:idempotent: true
:tags: []
- :name: gcp_cluster:cluster_configure_istio
+ :worker_name: ClusterConfigureIstioWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -596,6 +720,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_install_app
+ :worker_name: ClusterInstallAppWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -604,6 +729,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_patch_app
+ :worker_name: ClusterPatchAppWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -612,6 +738,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_provision
+ :worker_name: ClusterProvisionWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -620,6 +747,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_update_app
+ :worker_name: ClusterUpdateAppWorker
:feature_category: :kubernetes_management
:has_external_dependencies:
:urgency: :low
@@ -628,6 +756,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_upgrade_app
+ :worker_name: ClusterUpgradeAppWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -636,6 +765,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_installation
+ :worker_name: ClusterWaitForAppInstallationWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -644,6 +774,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_update
+ :worker_name: ClusterWaitForAppUpdateWorker
:feature_category: :kubernetes_management
:has_external_dependencies:
:urgency: :low
@@ -652,6 +783,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_ingress_ip_address
+ :worker_name: ClusterWaitForIngressIpAddressWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -660,6 +792,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_activate_service
+ :worker_name: Clusters::Applications::ActivateServiceWorker
:feature_category: :kubernetes_management
:has_external_dependencies:
:urgency: :low
@@ -668,6 +801,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_deactivate_service
+ :worker_name: Clusters::Applications::DeactivateServiceWorker
:feature_category: :kubernetes_management
:has_external_dependencies:
:urgency: :low
@@ -676,6 +810,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_uninstall
+ :worker_name: Clusters::Applications::UninstallWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -684,6 +819,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_wait_for_uninstall_app
+ :worker_name: Clusters::Applications::WaitForUninstallAppWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -692,6 +828,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_app
+ :worker_name: Clusters::Cleanup::AppWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -700,6 +837,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_project_namespace
+ :worker_name: Clusters::Cleanup::ProjectNamespaceWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -708,6 +846,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_service_account
+ :worker_name: Clusters::Cleanup::ServiceAccountWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -716,6 +855,7 @@
:idempotent:
:tags: []
- :name: gcp_cluster:wait_for_cluster_creation
+ :worker_name: WaitForClusterCreationWorker
:feature_category: :kubernetes_management
:has_external_dependencies: true
:urgency: :low
@@ -724,6 +864,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_diff_note
+ :worker_name: Gitlab::GithubImport::ImportDiffNoteWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -732,6 +873,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_issue
+ :worker_name: Gitlab::GithubImport::ImportIssueWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -740,6 +882,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_lfs_object
+ :worker_name: Gitlab::GithubImport::ImportLfsObjectWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -748,6 +891,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_note
+ :worker_name: Gitlab::GithubImport::ImportNoteWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -756,6 +900,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_pull_request
+ :worker_name: Gitlab::GithubImport::ImportPullRequestWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -764,22 +909,27 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_import_pull_request_merged_by
+ :worker_name: Gitlab::GithubImport::ImportPullRequestMergedByWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: github_importer:github_import_import_pull_request_review
+ :worker_name: Gitlab::GithubImport::ImportPullRequestReviewWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: github_importer:github_import_refresh_import_jid
+ :worker_name: Gitlab::GithubImport::RefreshImportJidWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -788,6 +938,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_finish_import
+ :worker_name: Gitlab::GithubImport::Stage::FinishImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -796,6 +947,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_base_data
+ :worker_name: Gitlab::GithubImport::Stage::ImportBaseDataWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -804,6 +956,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_issues_and_diff_notes
+ :worker_name: Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -812,6 +965,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_lfs_objects
+ :worker_name: Gitlab::GithubImport::Stage::ImportLfsObjectsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -820,6 +974,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_notes
+ :worker_name: Gitlab::GithubImport::Stage::ImportNotesWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -828,6 +983,7 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests
+ :worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -836,22 +992,27 @@
:idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests_merged_by
+ :worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: github_importer:github_import_stage_import_pull_requests_reviews
+ :worker_name: Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: github_importer:github_import_stage_import_repository
+ :worker_name: Gitlab::GithubImport::Stage::ImportRepositoryWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -860,38 +1021,47 @@
:idempotent:
:tags: []
- :name: hashed_storage:hashed_storage_migrator
+ :worker_name: HashedStorage::MigratorWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: hashed_storage:hashed_storage_project_migrate
+ :worker_name: HashedStorage::ProjectMigrateWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: hashed_storage:hashed_storage_project_rollback
+ :worker_name: HashedStorage::ProjectRollbackWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: hashed_storage:hashed_storage_rollbacker
+ :worker_name: HashedStorage::RollbackerWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_gitlab_com
- :name: incident_management:clusters_applications_check_prometheus_health
+ :worker_name: Clusters::Applications::CheckPrometheusHealthWorker
:feature_category: :incident_management
:has_external_dependencies: true
:urgency: :low
@@ -900,14 +1070,17 @@
:idempotent: true
:tags: []
- :name: incident_management:incident_management_add_severity_system_note
+ :worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: incident_management:incident_management_pager_duty_process_incident
+ :worker_name: IncidentManagement::PagerDuty::ProcessIncidentWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
@@ -916,6 +1089,7 @@
:idempotent:
:tags: []
- :name: incident_management:incident_management_process_alert
+ :worker_name: IncidentManagement::ProcessAlertWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
@@ -923,7 +1097,17 @@
:weight: 2
:idempotent:
:tags: []
+- :name: incident_management:incident_management_process_alert_worker_v2
+ :worker_name: IncidentManagement::ProcessAlertWorkerV2
+ :feature_category: :incident_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 2
+ :idempotent: true
+ :tags: []
- :name: incident_management:incident_management_process_prometheus_alert
+ :worker_name: IncidentManagement::ProcessPrometheusAlertWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
@@ -932,6 +1116,7 @@
:idempotent:
:tags: []
- :name: jira_connect:jira_connect_sync_branch
+ :worker_name: JiraConnect::SyncBranchWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
@@ -940,30 +1125,37 @@
:idempotent: true
:tags: []
- :name: jira_connect:jira_connect_sync_builds
+ :worker_name: JiraConnect::SyncBuildsWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: jira_connect:jira_connect_sync_deployments
+ :worker_name: JiraConnect::SyncDeploymentsWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: jira_connect:jira_connect_sync_feature_flags
+ :worker_name: JiraConnect::SyncFeatureFlagsWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: jira_connect:jira_connect_sync_merge_request
+ :worker_name: JiraConnect::SyncMergeRequestWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
@@ -972,14 +1164,17 @@
:idempotent: true
:tags: []
- :name: jira_connect:jira_connect_sync_project
+ :worker_name: JiraConnect::SyncProjectWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: jira_importer:jira_import_advance_stage
+ :worker_name: Gitlab::JiraImport::AdvanceStageWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -988,6 +1183,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_import_issue
+ :worker_name: Gitlab::JiraImport::ImportIssueWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -996,6 +1192,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_finish_import
+ :worker_name: Gitlab::JiraImport::Stage::FinishImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1004,6 +1201,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_attachments
+ :worker_name: Gitlab::JiraImport::Stage::ImportAttachmentsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1012,6 +1210,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_issues
+ :worker_name: Gitlab::JiraImport::Stage::ImportIssuesWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1020,6 +1219,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_labels
+ :worker_name: Gitlab::JiraImport::Stage::ImportLabelsWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1028,6 +1228,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_notes
+ :worker_name: Gitlab::JiraImport::Stage::ImportNotesWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1036,6 +1237,7 @@
:idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_start_import
+ :worker_name: Gitlab::JiraImport::Stage::StartImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1044,6 +1246,7 @@
:idempotent:
:tags: []
- :name: mail_scheduler:mail_scheduler_issue_due
+ :worker_name: MailScheduler::IssueDueWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1052,6 +1255,7 @@
:idempotent:
:tags: []
- :name: mail_scheduler:mail_scheduler_notification_service
+ :worker_name: MailScheduler::NotificationServiceWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1060,6 +1264,7 @@
:idempotent:
:tags: []
- :name: object_pool:object_pool_create
+ :worker_name: ObjectPool::CreateWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
@@ -1068,6 +1273,7 @@
:idempotent:
:tags: []
- :name: object_pool:object_pool_destroy
+ :worker_name: ObjectPool::DestroyWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
@@ -1076,6 +1282,7 @@
:idempotent:
:tags: []
- :name: object_pool:object_pool_join
+ :worker_name: ObjectPool::JoinWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
@@ -1084,6 +1291,7 @@
:idempotent:
:tags: []
- :name: object_pool:object_pool_schedule_join
+ :worker_name: ObjectPool::ScheduleJoinWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
@@ -1092,6 +1300,7 @@
:idempotent:
:tags: []
- :name: object_storage:object_storage_background_move
+ :worker_name: ObjectStorage::BackgroundMoveWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
@@ -1100,6 +1309,7 @@
:idempotent:
:tags: []
- :name: object_storage:object_storage_migrate_uploads
+ :worker_name: ObjectStorage::MigrateUploadsWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
@@ -1107,23 +1317,38 @@
:weight: 1
:idempotent:
:tags: []
+- :name: package_repositories:packages_debian_process_changes
+ :worker_name: Packages::Debian::ProcessChangesWorker
+ :feature_category: :package_registry
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags:
+ - :exclude_from_kubernetes
- :name: package_repositories:packages_go_sync_packages
+ :worker_name: Packages::Go::SyncPackagesWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: package_repositories:packages_maven_metadata_sync
+ :worker_name: Packages::Maven::Metadata::SyncWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: package_repositories:packages_nuget_extraction
+ :worker_name: Packages::Nuget::ExtractionWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
@@ -1132,14 +1357,17 @@
:idempotent:
:tags: []
- :name: package_repositories:packages_rubygems_extraction
+ :worker_name: Packages::Rubygems::ExtractionWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent: true
- :tags: []
+ :idempotent:
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_background:archive_trace
+ :worker_name: ArchiveTraceWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1148,6 +1376,7 @@
:idempotent:
:tags: []
- :name: pipeline_background:ci_build_trace_chunk_flush
+ :worker_name: Ci::BuildTraceChunkFlushWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1156,6 +1385,7 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_daily_build_group_report_results
+ :worker_name: Ci::DailyBuildGroupReportResultsWorker
:feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
@@ -1164,22 +1394,27 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_pipeline_artifacts_coverage_report
+ :worker_name: Ci::PipelineArtifacts::CoverageReportWorker
:feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_background:ci_pipeline_artifacts_create_quality_report
+ :worker_name: Ci::PipelineArtifacts::CreateQualityReportWorker
:feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_background:ci_pipeline_success_unlock_artifacts
+ :worker_name: Ci::PipelineSuccessUnlockArtifactsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1188,6 +1423,7 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_ref_delete_unlock_artifacts
+ :worker_name: Ci::RefDeleteUnlockArtifactsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1196,14 +1432,17 @@
:idempotent: true
:tags: []
- :name: pipeline_background:ci_test_failure_history
+ :worker_name: Ci::TestFailureHistoryWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_cache:expire_job_cache
+ :worker_name: ExpireJobCacheWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1212,6 +1451,7 @@
:idempotent: true
:tags: []
- :name: pipeline_cache:expire_pipeline_cache
+ :worker_name: ExpirePipelineCacheWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1220,6 +1460,7 @@
:idempotent: true
:tags: []
- :name: pipeline_creation:create_pipeline
+ :worker_name: CreatePipelineWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1228,6 +1469,7 @@
:idempotent:
:tags: []
- :name: pipeline_creation:merge_requests_create_pipeline
+ :worker_name: MergeRequests::CreatePipelineWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1236,6 +1478,7 @@
:idempotent: true
:tags: []
- :name: pipeline_creation:run_pipeline_schedule
+ :worker_name: RunPipelineScheduleWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1244,6 +1487,7 @@
:idempotent:
:tags: []
- :name: pipeline_default:ci_create_cross_project_pipeline
+ :worker_name: Ci::CreateCrossProjectPipelineWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1252,22 +1496,36 @@
:idempotent:
:tags: []
- :name: pipeline_default:ci_drop_pipeline
+ :worker_name: Ci::DropPipelineWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_default:ci_merge_requests_add_todo_when_build_fails
+ :worker_name: Ci::MergeRequests::AddTodoWhenBuildFailsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pipeline_default:ci_pipeline_bridge_status
+ :worker_name: Ci::PipelineBridgeStatusWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :cpu
+ :weight: 3
+ :idempotent:
+ :tags: []
+- :name: pipeline_default:ci_retry_pipeline
+ :worker_name: Ci::RetryPipelineWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1276,6 +1534,7 @@
:idempotent:
:tags: []
- :name: pipeline_default:pipeline_metrics
+ :worker_name: PipelineMetricsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1284,6 +1543,7 @@
:idempotent:
:tags: []
- :name: pipeline_default:pipeline_notification
+ :worker_name: PipelineNotificationWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1292,6 +1552,7 @@
:idempotent:
:tags: []
- :name: pipeline_hooks:build_hooks
+ :worker_name: BuildHooksWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1300,6 +1561,7 @@
:idempotent:
:tags: []
- :name: pipeline_hooks:pipeline_hooks
+ :worker_name: PipelineHooksWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1308,6 +1570,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:build_finished
+ :worker_name: BuildFinishedWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1316,6 +1579,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:build_queue
+ :worker_name: BuildQueueWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1324,6 +1588,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:build_success
+ :worker_name: BuildSuccessWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1332,6 +1597,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:ci_build_prepare
+ :worker_name: Ci::BuildPrepareWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1340,6 +1606,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:ci_build_schedule
+ :worker_name: Ci::BuildScheduleWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1348,6 +1615,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:ci_initial_pipeline_process
+ :worker_name: Ci::InitialPipelineProcessWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1356,6 +1624,7 @@
:idempotent: true
:tags: []
- :name: pipeline_processing:ci_resource_groups_assign_resource_from_resource_group
+ :worker_name: Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
@@ -1364,6 +1633,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:pipeline_process
+ :worker_name: PipelineProcessWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1372,6 +1642,7 @@
:idempotent:
:tags: []
- :name: pipeline_processing:pipeline_update
+ :worker_name: PipelineUpdateWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1380,6 +1651,7 @@
:idempotent: true
:tags: []
- :name: pipeline_processing:stage_update
+ :worker_name: StageUpdateWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1388,6 +1660,7 @@
:idempotent: true
:tags: []
- :name: pipeline_processing:update_head_pipeline_for_merge_request
+ :worker_name: UpdateHeadPipelineForMergeRequestWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
@@ -1396,6 +1669,7 @@
:idempotent: true
:tags: []
- :name: repository_check:repository_check_batch
+ :worker_name: RepositoryCheck::BatchWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1404,6 +1678,7 @@
:idempotent:
:tags: []
- :name: repository_check:repository_check_clear
+ :worker_name: RepositoryCheck::ClearWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1412,6 +1687,7 @@
:idempotent:
:tags: []
- :name: repository_check:repository_check_single_repository
+ :worker_name: RepositoryCheck::SingleRepositoryWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1420,6 +1696,7 @@
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_issue
+ :worker_name: TodosDestroyer::ConfidentialIssueWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1428,14 +1705,17 @@
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_destroyed_issuable
+ :worker_name: TodosDestroyer::DestroyedIssuableWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: todos_destroyer:todos_destroyer_entity_leave
+ :worker_name: TodosDestroyer::EntityLeaveWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1444,6 +1724,7 @@
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_group_private
+ :worker_name: TodosDestroyer::GroupPrivateWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1452,6 +1733,7 @@
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_private_features
+ :worker_name: TodosDestroyer::PrivateFeaturesWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1460,6 +1742,7 @@
:idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_project_private
+ :worker_name: TodosDestroyer::ProjectPrivateWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1468,6 +1751,7 @@
:idempotent:
:tags: []
- :name: unassign_issuables:members_destroyer_unassign_issuables
+ :worker_name: MembersDestroyer::UnassignIssuablesWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -1476,6 +1760,7 @@
:idempotent: true
:tags: []
- :name: update_namespace_statistics:namespaces_root_statistics
+ :worker_name: Namespaces::RootStatisticsWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1484,6 +1769,7 @@
:idempotent: true
:tags: []
- :name: update_namespace_statistics:namespaces_schedule_aggregation
+ :worker_name: Namespaces::ScheduleAggregationWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1492,30 +1778,37 @@
:idempotent: true
:tags: []
- :name: analytics_instance_statistics_counter_job
+ :worker_name: Analytics::InstanceStatistics::CounterJobWorker
:feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: analytics_usage_trends_counter_job
+ :worker_name: Analytics::UsageTrends::CounterJobWorker
:feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: approve_blocked_pending_approval_users
+ :worker_name: ApproveBlockedPendingApprovalUsersWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: authorized_keys
+ :worker_name: AuthorizedKeysWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -1524,6 +1817,7 @@
:idempotent: true
:tags: []
- :name: authorized_projects
+ :worker_name: AuthorizedProjectsWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :high
@@ -1532,6 +1826,7 @@
:idempotent: true
:tags: []
- :name: background_migration
+ :worker_name: BackgroundMigrationWorker
:feature_category: :database
:has_external_dependencies:
:urgency: :throttled
@@ -1540,30 +1835,56 @@
:idempotent:
:tags: []
- :name: bulk_import
+ :worker_name: BulkImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: bulk_imports_entity
+ :worker_name: BulkImports::EntityWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
+ :tags:
+ - :exclude_from_kubernetes
+- :name: bulk_imports_export_request
+ :worker_name: BulkImports::ExportRequestWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
:tags: []
- :name: bulk_imports_pipeline
+ :worker_name: BulkImports::PipelineWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
+- :name: bulk_imports_relation_export
+ :worker_name: BulkImports::RelationExportWorker
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags:
+ - :exclude_from_kubernetes
- :name: chat_notification
+ :worker_name: ChatNotificationWorker
:feature_category: :chatops
:has_external_dependencies: true
:urgency: :low
@@ -1572,14 +1893,17 @@
:idempotent:
:tags: []
- :name: ci_delete_objects
+ :worker_name: Ci::DeleteObjectsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: create_commit_signature
+ :worker_name: CreateCommitSignatureWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1588,6 +1912,7 @@
:idempotent: true
:tags: []
- :name: create_note_diff_file
+ :worker_name: CreateNoteDiffFileWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -1596,6 +1921,7 @@
:idempotent:
:tags: []
- :name: default
+ :worker_name:
:feature_category:
:has_external_dependencies:
:urgency:
@@ -1604,6 +1930,7 @@
:idempotent:
:tags: []
- :name: delete_diff_files
+ :worker_name: DeleteDiffFilesWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -1612,6 +1939,7 @@
:idempotent:
:tags: []
- :name: delete_merged_branches
+ :worker_name: DeleteMergedBranchesWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1620,6 +1948,7 @@
:idempotent:
:tags: []
- :name: delete_stored_files
+ :worker_name: DeleteStoredFilesWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
@@ -1628,6 +1957,7 @@
:idempotent:
:tags: []
- :name: delete_user
+ :worker_name: DeleteUserWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -1636,14 +1966,17 @@
:idempotent:
:tags: []
- :name: design_management_copy_design_collection
+ :worker_name: DesignManagement::CopyDesignCollectionWorker
:feature_category: :design_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: design_management_new_version
+ :worker_name: DesignManagement::NewVersionWorker
:feature_category: :design_management
:has_external_dependencies:
:urgency: :low
@@ -1652,14 +1985,17 @@
:idempotent:
:tags: []
- :name: destroy_pages_deployments
+ :worker_name: DestroyPagesDeploymentsWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: detect_repository_languages
+ :worker_name: DetectRepositoryLanguagesWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1668,22 +2004,27 @@
:idempotent:
:tags: []
- :name: disallow_two_factor_for_group
+ :worker_name: DisallowTwoFactorForGroupWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: disallow_two_factor_for_subgroups
+ :worker_name: DisallowTwoFactorForSubgroupsWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: email_receiver
+ :worker_name: EmailReceiverWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :high
@@ -1692,6 +2033,7 @@
:idempotent:
:tags: []
- :name: emails_on_push
+ :worker_name: EmailsOnPushWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1700,14 +2042,17 @@
:idempotent:
:tags: []
- :name: environments_canary_ingress_update
+ :worker_name: Environments::CanaryIngress::UpdateWorker
:feature_category: :continuous_delivery
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: error_tracking_issue_link
+ :worker_name: ErrorTrackingIssueLinkWorker
:feature_category: :error_tracking
:has_external_dependencies: true
:urgency: :low
@@ -1716,14 +2061,17 @@
:idempotent:
:tags: []
- :name: experiments_record_conversion_event
+ :worker_name: Experiments::RecordConversionEventWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: expire_build_instance_artifacts
+ :worker_name: ExpireBuildInstanceArtifactsWorker
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -1732,6 +2080,7 @@
:idempotent:
:tags: []
- :name: export_csv
+ :worker_name: ExportCsvWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1740,6 +2089,7 @@
:idempotent:
:tags: []
- :name: external_service_reactive_caching
+ :worker_name: ExternalServiceReactiveCachingWorker
:feature_category: :not_owned
:has_external_dependencies: true
:urgency: :low
@@ -1748,6 +2098,7 @@
:idempotent:
:tags: []
- :name: file_hook
+ :worker_name: FileHookWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
@@ -1756,22 +2107,17 @@
:idempotent:
:tags: []
- :name: flush_counter_increments
+ :worker_name: FlushCounterIncrementsWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
-- :name: git_garbage_collect
- :feature_category: :gitaly
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: github_import_advance_stage
+ :worker_name: Gitlab::GithubImport::AdvanceStageWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1780,14 +2126,17 @@
:idempotent:
:tags: []
- :name: gitlab_performance_bar_stats
+ :worker_name: GitlabPerformanceBarStatsWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: gitlab_shell
+ :worker_name: GitlabShellWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -1796,6 +2145,7 @@
:idempotent:
:tags: []
- :name: group_destroy
+ :worker_name: GroupDestroyWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
@@ -1804,7 +2154,9 @@
:idempotent:
:tags:
- :requires_disk_io
+ - :exclude_from_kubernetes
- :name: group_export
+ :worker_name: GroupExportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1813,6 +2165,7 @@
:idempotent:
:tags: []
- :name: group_import
+ :worker_name: GroupImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -1821,6 +2174,7 @@
:idempotent:
:tags: []
- :name: import_issues_csv
+ :worker_name: ImportIssuesCsvWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1829,6 +2183,7 @@
:idempotent: true
:tags: []
- :name: invalid_gpg_signature_update
+ :worker_name: InvalidGpgSignatureUpdateWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -1837,6 +2192,7 @@
:idempotent:
:tags: []
- :name: irker
+ :worker_name: IrkerWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
@@ -1845,6 +2201,7 @@
:idempotent:
:tags: []
- :name: issuable_export_csv
+ :worker_name: IssuableExportCsvWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
@@ -1852,7 +2209,26 @@
:weight: 1
:idempotent:
:tags: []
+- :name: issuable_label_links_destroy
+ :worker_name: Issuable::LabelLinksDestroyWorker
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: issuables_clear_groups_issue_counter
+ :worker_name: Issuables::ClearGroupsIssueCounterWorker
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: issue_placement
+ :worker_name: IssuePlacementWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :high
@@ -1861,14 +2237,17 @@
:idempotent: true
:tags: []
- :name: issue_rebalancing
+ :worker_name: IssueRebalancingWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: mailers
+ :worker_name: ActionMailer::MailDeliveryJob
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: low
@@ -1877,22 +2256,26 @@
:idempotent:
:tags: []
- :name: merge
+ :worker_name: MergeWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent: true
:tags: []
- :name: merge_request_cleanup_refs
+ :worker_name: MergeRequestCleanupRefsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: merge_request_mergeability_check
+ :worker_name: MergeRequestMergeabilityCheckWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -1901,6 +2284,7 @@
:idempotent: true
:tags: []
- :name: merge_requests_assignees_change
+ :worker_name: MergeRequests::AssigneesChangeWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -1909,6 +2293,7 @@
:idempotent: true
:tags: []
- :name: merge_requests_delete_source_branch
+ :worker_name: MergeRequests::DeleteSourceBranchWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -1917,6 +2302,7 @@
:idempotent: true
:tags: []
- :name: merge_requests_handle_assignees_change
+ :worker_name: MergeRequests::HandleAssigneesChangeWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :high
@@ -1925,6 +2311,7 @@
:idempotent: true
:tags: []
- :name: merge_requests_resolve_todos
+ :worker_name: MergeRequests::ResolveTodosWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :high
@@ -1933,6 +2320,7 @@
:idempotent: true
:tags: []
- :name: metrics_dashboard_prune_old_annotations
+ :worker_name: Metrics::Dashboard::PruneOldAnnotationsWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
@@ -1941,14 +2329,17 @@
:idempotent: true
:tags: []
- :name: metrics_dashboard_sync_dashboards
+ :worker_name: Metrics::Dashboard::SyncDashboardsWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: migrate_external_diffs
+ :worker_name: MigrateExternalDiffsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :low
@@ -1957,6 +2348,7 @@
:idempotent:
:tags: []
- :name: namespaceless_project_destroy
+ :worker_name: NamespacelessProjectDestroyWorker
:feature_category: :authentication_and_authorization
:has_external_dependencies:
:urgency: :low
@@ -1965,38 +2357,47 @@
:idempotent:
:tags: []
- :name: namespaces_onboarding_issue_created
+ :worker_name: Namespaces::OnboardingIssueCreatedWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: namespaces_onboarding_pipeline_created
+ :worker_name: Namespaces::OnboardingPipelineCreatedWorker
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: namespaces_onboarding_progress
+ :worker_name: Namespaces::OnboardingProgressWorker
:feature_category: :product_analytics
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: namespaces_onboarding_user_added
+ :worker_name: Namespaces::OnboardingUserAddedWorker
:feature_category: :users
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: new_issue
+ :worker_name: NewIssueWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :high
@@ -2005,6 +2406,7 @@
:idempotent:
:tags: []
- :name: new_merge_request
+ :worker_name: NewMergeRequestWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :high
@@ -2013,6 +2415,7 @@
:idempotent:
:tags: []
- :name: new_note
+ :worker_name: NewNoteWorker
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :high
@@ -2021,14 +2424,17 @@
:idempotent:
:tags: []
- :name: packages_composer_cache_update
+ :worker_name: Packages::Composer::CacheUpdateWorker
:feature_category: :package_registry
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pages
+ :worker_name: PagesWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -2037,7 +2443,9 @@
:idempotent:
:tags:
- :requires_disk_io
+ - :exclude_from_kubernetes
- :name: pages_domain_ssl_renewal
+ :worker_name: PagesDomainSslRenewalWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -2046,7 +2454,9 @@
:idempotent:
:tags:
- :requires_disk_io
+ - :exclude_from_kubernetes
- :name: pages_domain_verification
+ :worker_name: PagesDomainVerificationWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
@@ -2055,31 +2465,39 @@
:idempotent:
:tags:
- :requires_disk_io
+ - :exclude_from_kubernetes
- :name: pages_remove
+ :worker_name: PagesRemoveWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pages_transfer
+ :worker_name: PagesTransferWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: pages_update_configuration
+ :worker_name: PagesUpdateConfigurationWorker
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: phabricator_import_import_tasks
+ :worker_name: Gitlab::PhabricatorImport::ImportTasksWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
@@ -2088,6 +2506,7 @@
:idempotent:
:tags: []
- :name: post_receive
+ :worker_name: PostReceive
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -2096,6 +2515,7 @@
:idempotent:
:tags: []
- :name: process_commit
+ :worker_name: ProcessCommitWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -2104,6 +2524,7 @@
:idempotent: true
:tags: []
- :name: project_cache
+ :worker_name: ProjectCacheWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :high
@@ -2112,6 +2533,7 @@
:idempotent: true
:tags: []
- :name: project_daily_statistics
+ :worker_name: ProjectDailyStatisticsWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2120,6 +2542,7 @@
:idempotent:
:tags: []
- :name: project_destroy
+ :worker_name: ProjectDestroyWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2128,7 +2551,9 @@
:idempotent:
:tags:
- :requires_disk_io
+ - :exclude_from_kubernetes
- :name: project_export
+ :worker_name: ProjectExportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :throttled
@@ -2137,6 +2562,7 @@
:idempotent:
:tags: []
- :name: project_schedule_bulk_repository_shard_moves
+ :worker_name: ProjectScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2145,6 +2571,7 @@
:idempotent: true
:tags: []
- :name: project_service
+ :worker_name: ProjectServiceWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
@@ -2153,6 +2580,7 @@
:idempotent:
:tags: []
- :name: project_update_repository_storage
+ :worker_name: ProjectUpdateRepositoryStorageWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2161,22 +2589,27 @@
:idempotent: true
:tags: []
- :name: projects_git_garbage_collect
+ :worker_name: Projects::GitGarbageCollectWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: projects_post_creation
+ :worker_name: Projects::PostCreationWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: projects_schedule_bulk_repository_shard_moves
+ :worker_name: Projects::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2185,6 +2618,7 @@
:idempotent: true
:tags: []
- :name: projects_update_repository_storage
+ :worker_name: Projects::UpdateRepositoryStorageWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2193,6 +2627,7 @@
:idempotent: true
:tags: []
- :name: prometheus_create_default_alerts
+ :worker_name: Prometheus::CreateDefaultAlertsWorker
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :high
@@ -2201,6 +2636,7 @@
:idempotent: true
:tags: []
- :name: propagate_integration
+ :worker_name: PropagateIntegrationWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
@@ -2209,38 +2645,47 @@
:idempotent: true
:tags: []
- :name: propagate_integration_group
+ :worker_name: PropagateIntegrationGroupWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: propagate_integration_inherit
+ :worker_name: PropagateIntegrationInheritWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: propagate_integration_inherit_descendant
+ :worker_name: PropagateIntegrationInheritDescendantWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: propagate_integration_project
+ :worker_name: PropagateIntegrationProjectWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: propagate_service_template
+ :worker_name: PropagateServiceTemplateWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
@@ -2249,6 +2694,7 @@
:idempotent:
:tags: []
- :name: reactive_caching
+ :worker_name: ReactiveCachingWorker
:feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
@@ -2257,6 +2703,7 @@
:idempotent:
:tags: []
- :name: rebase
+ :worker_name: RebaseWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2265,14 +2712,17 @@
:idempotent:
:tags: []
- :name: releases_create_evidence
+ :worker_name: Releases::CreateEvidenceWorker
:feature_category: :release_evidence
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: remote_mirror_notification
+ :worker_name: RemoteMirrorNotificationWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2281,6 +2731,7 @@
:idempotent:
:tags: []
- :name: repository_cleanup
+ :worker_name: RepositoryCleanupWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2289,6 +2740,7 @@
:idempotent:
:tags: []
- :name: repository_fork
+ :worker_name: RepositoryForkWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2297,6 +2749,7 @@
:idempotent:
:tags: []
- :name: repository_import
+ :worker_name: RepositoryImportWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
@@ -2305,6 +2758,7 @@
:idempotent:
:tags: []
- :name: repository_remove_remote
+ :worker_name: RepositoryRemoveRemoteWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2313,6 +2767,7 @@
:idempotent:
:tags: []
- :name: repository_update_remote_mirror
+ :worker_name: RepositoryUpdateRemoteMirrorWorker
:feature_category: :source_code_management
:has_external_dependencies: true
:urgency: :low
@@ -2321,6 +2776,7 @@
:idempotent: true
:tags: []
- :name: self_monitoring_project_create
+ :worker_name: SelfMonitoringProjectCreateWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
@@ -2329,6 +2785,7 @@
:idempotent:
:tags: []
- :name: self_monitoring_project_delete
+ :worker_name: SelfMonitoringProjectDeleteWorker
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
@@ -2337,7 +2794,8 @@
:idempotent:
:tags: []
- :name: service_desk_email_receiver
- :feature_category: :issue_tracking
+ :worker_name: ServiceDeskEmailReceiverWorker
+ :feature_category: :service_desk
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2345,6 +2803,7 @@
:idempotent:
:tags: []
- :name: snippet_schedule_bulk_repository_shard_moves
+ :worker_name: SnippetScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2353,6 +2812,7 @@
:idempotent: true
:tags: []
- :name: snippet_update_repository_storage
+ :worker_name: SnippetUpdateRepositoryStorageWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2361,6 +2821,7 @@
:idempotent: true
:tags: []
- :name: snippets_schedule_bulk_repository_shard_moves
+ :worker_name: Snippets::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2369,6 +2830,7 @@
:idempotent: true
:tags: []
- :name: snippets_update_repository_storage
+ :worker_name: Snippets::UpdateRepositoryStorageWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
@@ -2377,6 +2839,7 @@
:idempotent: true
:tags: []
- :name: system_hook_push
+ :worker_name: SystemHookPushWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2385,6 +2848,7 @@
:idempotent:
:tags: []
- :name: update_external_pull_requests
+ :worker_name: UpdateExternalPullRequestsWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2393,6 +2857,7 @@
:idempotent:
:tags: []
- :name: update_highest_role
+ :worker_name: UpdateHighestRoleWorker
:feature_category: :utilization
:has_external_dependencies:
:urgency: :high
@@ -2401,6 +2866,7 @@
:idempotent: true
:tags: []
- :name: update_merge_requests
+ :worker_name: UpdateMergeRequestsWorker
:feature_category: :code_review
:has_external_dependencies:
:urgency: :high
@@ -2409,6 +2875,7 @@
:idempotent:
:tags: []
- :name: update_project_statistics
+ :worker_name: UpdateProjectStatisticsWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
@@ -2417,6 +2884,7 @@
:idempotent:
:tags: []
- :name: upload_checksum
+ :worker_name: UploadChecksumWorker
:feature_category: :geo_replication
:has_external_dependencies:
:urgency: :low
@@ -2424,7 +2892,18 @@
:weight: 1
:idempotent:
:tags: []
+- :name: users_update_open_issue_count
+ :worker_name: Users::UpdateOpenIssueCountWorker
+ :feature_category: :users
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags:
+ - :exclude_from_kubernetes
- :name: web_hook
+ :worker_name: WebHookWorker
:feature_category: :integrations
:has_external_dependencies: true
:urgency: :low
@@ -2433,22 +2912,27 @@
:idempotent:
:tags: []
- :name: web_hooks_destroy
+ :worker_name: WebHooks::DestroyWorker
:feature_category: :integrations
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: wikis_git_garbage_collect
+ :worker_name: Wikis::GitGarbageCollectWorker
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :exclude_from_kubernetes
- :name: x509_certificate_revoke
+ :worker_name: X509CertificateRevokeWorker
:feature_category: :source_code_management
:has_external_dependencies:
:urgency: :low
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 3ec92bc7635..083c01b166d 100644
--- a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
+++ b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
@@ -5,9 +5,12 @@ module Analytics
# This worker will be removed in 14.0
class CountJobTriggerWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :devops_reports
+ tags :exclude_from_kubernetes
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 4beed8a3e2f..a4dda45ff72 100644
--- a/app/workers/analytics/instance_statistics/counter_job_worker.rb
+++ b/app/workers/analytics/instance_statistics/counter_job_worker.rb
@@ -6,8 +6,11 @@ module Analytics
class CounterJobWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :devops_reports
urgency :low
+ tags :exclude_from_kubernetes
idempotent!
diff --git a/app/workers/analytics/usage_trends/count_job_trigger_worker.rb b/app/workers/analytics/usage_trends/count_job_trigger_worker.rb
index 37f5c19d64c..f2d4404a964 100644
--- a/app/workers/analytics/usage_trends/count_job_trigger_worker.rb
+++ b/app/workers/analytics/usage_trends/count_job_trigger_worker.rb
@@ -5,11 +5,14 @@ module Analytics
class CountJobTriggerWorker
extend ::Gitlab::Utils::Override
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
DEFAULT_DELAY = 3.minutes.freeze
feature_category :devops_reports
+ tags :exclude_from_kubernetes
urgency :low
idempotent!
diff --git a/app/workers/analytics/usage_trends/counter_job_worker.rb b/app/workers/analytics/usage_trends/counter_job_worker.rb
index 275c6ac2de2..f4dc497d25f 100644
--- a/app/workers/analytics/usage_trends/counter_job_worker.rb
+++ b/app/workers/analytics/usage_trends/counter_job_worker.rb
@@ -6,8 +6,11 @@ module Analytics
extend ::Gitlab::Utils::Override
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :devops_reports
urgency :low
+ tags :exclude_from_kubernetes
idempotent!
diff --git a/app/workers/approve_blocked_pending_approval_users_worker.rb b/app/workers/approve_blocked_pending_approval_users_worker.rb
index 8ca61d68bfd..ff72aaad3ce 100644
--- a/app/workers/approve_blocked_pending_approval_users_worker.rb
+++ b/app/workers/approve_blocked_pending_approval_users_worker.rb
@@ -3,9 +3,12 @@
class ApproveBlockedPendingApprovalUsersWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
feature_category :users
+ tags :exclude_from_kubernetes
def perform(current_user_id)
current_user = User.find(current_user_id)
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index 3ddb5686bf2..629526ec17c 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -2,6 +2,8 @@
class ArchiveTraceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/authorized_keys_worker.rb b/app/workers/authorized_keys_worker.rb
index ab0e7fc4921..953f493ea2c 100644
--- a/app/workers/authorized_keys_worker.rb
+++ b/app/workers/authorized_keys_worker.rb
@@ -3,6 +3,8 @@
class AuthorizedKeysWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
PERMITTED_ACTIONS = %w[add_key remove_key].freeze
feature_category :source_code_management
diff --git a/app/workers/authorized_project_update/periodic_recalculate_worker.rb b/app/workers/authorized_project_update/periodic_recalculate_worker.rb
index 78ffdbca4d6..2f6a9c42c0c 100644
--- a/app/workers/authorized_project_update/periodic_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/periodic_recalculate_worker.rb
@@ -3,6 +3,8 @@
module AuthorizedProjectUpdate
class PeriodicRecalculateWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# This worker does not perform work scoped to a context
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
diff --git a/app/workers/authorized_project_update/project_create_worker.rb b/app/workers/authorized_project_update/project_create_worker.rb
index 651849b57ec..52b740b4efe 100644
--- a/app/workers/authorized_project_update/project_create_worker.rb
+++ b/app/workers/authorized_project_update/project_create_worker.rb
@@ -4,6 +4,8 @@ module AuthorizedProjectUpdate
class ProjectCreateWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :authentication_and_authorization
urgency :low
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/project_group_link_create_worker.rb b/app/workers/authorized_project_update/project_group_link_create_worker.rb
index dd24a9602bb..d887a2ce25f 100644
--- a/app/workers/authorized_project_update/project_group_link_create_worker.rb
+++ b/app/workers/authorized_project_update/project_group_link_create_worker.rb
@@ -4,6 +4,8 @@ module AuthorizedProjectUpdate
class ProjectGroupLinkCreateWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :authentication_and_authorization
urgency :low
queue_namespace :authorized_project_update
diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
index 6635c322ab8..2e4e2dd3232 100644
--- a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
@@ -15,6 +15,8 @@ module AuthorizedProjectUpdate
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :authentication_and_authorization
urgency :low
queue_namespace :authorized_project_update
@@ -22,7 +24,7 @@ module AuthorizedProjectUpdate
# `data_consistency :delayed` and not `idempotent!`
# See https://gitlab.com/gitlab-org/gitlab/-/issues/325291
deduplicate :until_executing, including_scheduled: true
- data_consistency :delayed, feature_flag: :periodic_project_authorization_update_via_replica
+ data_consistency :delayed, feature_flag: :delayed_consistency_for_user_refresh_over_range_worker
def perform(start_user_id, end_user_id)
if Feature.enabled?(:periodic_project_authorization_update_via_replica)
@@ -30,12 +32,17 @@ module AuthorizedProjectUpdate
enqueue_project_authorizations_refresh(user) if project_authorizations_needs_refresh?(user)
end
else
+ use_primary_database
AuthorizedProjectUpdate::RecalculateForUserRangeService.new(start_user_id, end_user_id).execute
end
end
private
+ def use_primary_database
+ # no-op in CE, overriden in EE
+ end
+
def project_authorizations_needs_refresh?(user)
AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(user).needs_refresh?
end
@@ -47,3 +54,5 @@ module AuthorizedProjectUpdate
end
end
end
+
+AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker.prepend_mod_with('AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker')
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 6e07d6d0f71..a1068117e59 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -2,6 +2,8 @@
class AuthorizedProjectsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
prepend WaitableWorker
feature_category :authentication_and_authorization
diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb
index bae08cf9e18..43377382e82 100644
--- a/app/workers/auto_devops/disable_worker.rb
+++ b/app/workers/auto_devops/disable_worker.rb
@@ -3,6 +3,8 @@
module AutoDevops
class DisableWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include AutoDevopsQueue
def perform(pipeline_id)
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
index 2599c76c900..dda0e970834 100644
--- a/app/workers/auto_merge_process_worker.rb
+++ b/app/workers/auto_merge_process_worker.rb
@@ -3,6 +3,8 @@
class AutoMergeProcessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :auto_merge
feature_category :continuous_delivery
worker_resource_boundary :cpu
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index 70c4ad53726..6b1f10f75b8 100644
--- a/app/workers/background_migration_worker.rb
+++ b/app/workers/background_migration_worker.rb
@@ -3,6 +3,8 @@
class BackgroundMigrationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :database
urgency :throttled
loggable_arguments 0, 1
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index aeda8d113ac..a3eaacec8a2 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -2,6 +2,8 @@
class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
@@ -34,7 +36,6 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
- ExpirePipelineCacheWorker.perform_async(build.pipeline_id)
ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
if build.failed?
@@ -57,4 +58,4 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
end
end
-BuildFinishedWorker.prepend_if_ee('EE::BuildFinishedWorker')
+BuildFinishedWorker.prepend_mod_with('BuildFinishedWorker')
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index 5e05063f058..be79d6b2afb 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -2,6 +2,8 @@
class BuildHooksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_hooks
@@ -9,6 +11,16 @@ class BuildHooksWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high
data_consistency :delayed, feature_flag: :load_balancing_for_build_hooks_worker
+ DATA_CONSISTENCY_DELAY = 3
+
+ def self.perform_async(*args)
+ if Feature.enabled?(:delayed_perform_for_build_hooks_worker, default_enabled: :yaml)
+ perform_in(DATA_CONSISTENCY_DELAY.seconds, *args)
+ else
+ super
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.includes({ runner: :tags })
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index b71afbbeb8f..e9bb2d88a81 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -2,6 +2,8 @@
class BuildQueueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e4a2dd500cc..531e7e5a5fe 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -2,6 +2,8 @@
class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index b4b9d9b05c1..8ad31c68374 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -4,6 +4,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :importers
+ tags :exclude_from_kubernetes
sidekiq_options retry: false, dead: false
@@ -23,13 +24,14 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
created_entities.first(next_batch_size).each do |entity|
create_pipeline_tracker_for(entity)
+ BulkImports::ExportRequestWorker.perform_async(entity.id)
BulkImports::EntityWorker.perform_async(entity.id)
entity.start!
end
re_enqueue
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, bulk_import_id: @bulk_import&.id)
@bulk_import&.fail_op
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index 7f173b738cf..e7fce112ee1 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -5,6 +5,7 @@ module BulkImports
include ApplicationWorker
feature_category :importers
+ tags :exclude_from_kubernetes
sidekiq_options retry: false, dead: false
@@ -26,7 +27,7 @@ module BulkImports
entity_id
)
end
- rescue => e
+ rescue StandardError => e
logger.error(
worker: self.class.name,
entity_id: entity_id,
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
new file mode 100644
index 00000000000..cccc24d3bdc
--- /dev/null
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportRequestWorker
+ include ApplicationWorker
+
+ idempotent!
+ worker_has_external_dependencies!
+ feature_category :importers
+
+ GROUP_EXPORTED_URL_PATH = "/groups/%s/export_relations"
+
+ def perform(entity_id)
+ entity = BulkImports::Entity.find(entity_id)
+
+ request_export(entity)
+ end
+
+ private
+
+ def request_export(entity)
+ http_client(entity.bulk_import.configuration)
+ .post(GROUP_EXPORTED_URL_PATH % entity.encoded_source_full_path)
+ end
+
+ def http_client(configuration)
+ @client ||= Clients::Http.new(
+ uri: configuration.url,
+ token: configuration.access_token
+ )
+ end
+ end
+end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index a6de3c36205..256301bf097 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -5,6 +5,7 @@ module BulkImports
include ApplicationWorker
feature_category :importers
+ tags :exclude_from_kubernetes
sidekiq_options retry: false, dead: false
@@ -46,7 +47,7 @@ module BulkImports
pipeline_tracker.pipeline_class.new(context).run
pipeline_tracker.finish!
- rescue => e
+ rescue StandardError => e
pipeline_tracker.fail_op!
logger.error(
diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb
new file mode 100644
index 00000000000..9d9449e3a1b
--- /dev/null
+++ b/app/workers/bulk_imports/relation_export_worker.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class RelationExportWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ idempotent!
+ loggable_arguments 2, 3
+ feature_category :importers
+ tags :exclude_from_kubernetes
+ sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+
+ def perform(user_id, portable_id, portable_class, relation)
+ user = User.find(user_id)
+ portable = portable(portable_id, portable_class)
+
+ RelationExportService.new(user, portable, relation, jid).execute
+ end
+
+ private
+
+ def portable(portable_id, portable_class)
+ portable_class.classify.constantize.find(portable_id)
+ end
+ end
+end
diff --git a/app/workers/chaos/cpu_spin_worker.rb b/app/workers/chaos/cpu_spin_worker.rb
index 0b565e0d49c..f8900abc764 100644
--- a/app/workers/chaos/cpu_spin_worker.rb
+++ b/app/workers/chaos/cpu_spin_worker.rb
@@ -3,6 +3,8 @@
module Chaos
class CpuSpinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ChaosQueue
def perform(duration_s)
diff --git a/app/workers/chaos/db_spin_worker.rb b/app/workers/chaos/db_spin_worker.rb
index 099660d440c..9b5d06414a9 100644
--- a/app/workers/chaos/db_spin_worker.rb
+++ b/app/workers/chaos/db_spin_worker.rb
@@ -3,6 +3,8 @@
module Chaos
class DbSpinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ChaosQueue
def perform(duration_s, interval_s)
diff --git a/app/workers/chaos/leak_mem_worker.rb b/app/workers/chaos/leak_mem_worker.rb
index b77d1a20541..788009962db 100644
--- a/app/workers/chaos/leak_mem_worker.rb
+++ b/app/workers/chaos/leak_mem_worker.rb
@@ -3,6 +3,8 @@
module Chaos
class LeakMemWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ChaosQueue
def perform(memory_mb, duration_s)
diff --git a/app/workers/chaos/sleep_worker.rb b/app/workers/chaos/sleep_worker.rb
index 6887258e961..b9ff5546384 100644
--- a/app/workers/chaos/sleep_worker.rb
+++ b/app/workers/chaos/sleep_worker.rb
@@ -3,6 +3,8 @@
module Chaos
class SleepWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ChaosQueue
def perform(duration_s)
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 0171c1d482d..c748bc33ada 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -3,6 +3,8 @@
module Ci
class ArchiveTracesCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :continuous_integration
diff --git a/app/workers/ci/build_prepare_worker.rb b/app/workers/ci/build_prepare_worker.rb
index 7f640633070..f30e9d3b885 100644
--- a/app/workers/ci/build_prepare_worker.rb
+++ b/app/workers/ci/build_prepare_worker.rb
@@ -3,6 +3,8 @@
module Ci
class BuildPrepareWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
index 9231b40978d..570f5f28c3d 100644
--- a/app/workers/ci/build_schedule_worker.rb
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -3,6 +3,8 @@
module Ci
class BuildScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index a63b12c0d03..1e0da73e08d 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -3,6 +3,8 @@
module Ci
class BuildTraceChunkFlushWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
deduplicate :until_executed
diff --git a/app/workers/ci/create_cross_project_pipeline_worker.rb b/app/workers/ci/create_cross_project_pipeline_worker.rb
index 679574d9f60..4881ee12e5c 100644
--- a/app/workers/ci/create_cross_project_pipeline_worker.rb
+++ b/app/workers/ci/create_cross_project_pipeline_worker.rb
@@ -5,6 +5,7 @@ module Ci
include ::ApplicationWorker
include ::PipelineQueue
+ sidekiq_options retry: 3
worker_resource_boundary :cpu
def perform(bridge_id)
diff --git a/app/workers/ci/daily_build_group_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb
index 687cadc6366..b38bef3bcf8 100644
--- a/app/workers/ci/daily_build_group_report_results_worker.rb
+++ b/app/workers/ci/daily_build_group_report_results_worker.rb
@@ -3,6 +3,8 @@
module Ci
class DailyBuildGroupReportResultsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
feature_category :code_testing
diff --git a/app/workers/ci/delete_objects_worker.rb b/app/workers/ci/delete_objects_worker.rb
index d845ad61358..ff020a3b048 100644
--- a/app/workers/ci/delete_objects_worker.rb
+++ b/app/workers/ci/delete_objects_worker.rb
@@ -3,9 +3,12 @@
module Ci
class DeleteObjectsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include LimitedCapacity::Worker
feature_category :continuous_integration
+ tags :exclude_from_kubernetes
idempotent!
def perform_work(*args)
diff --git a/app/workers/ci/delete_unit_tests_worker.rb b/app/workers/ci/delete_unit_tests_worker.rb
new file mode 100644
index 00000000000..ddfc70c43d4
--- /dev/null
+++ b/app/workers/ci/delete_unit_tests_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ class DeleteUnitTestsWorker
+ include ApplicationWorker
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker does not perform work scoped to a context
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :continuous_integration
+ idempotent!
+
+ def perform
+ Ci::DeleteUnitTestsService.new.execute
+ end
+ end
+end
diff --git a/app/workers/ci/drop_pipeline_worker.rb b/app/workers/ci/drop_pipeline_worker.rb
index d19157a47e8..bc158433228 100644
--- a/app/workers/ci/drop_pipeline_worker.rb
+++ b/app/workers/ci/drop_pipeline_worker.rb
@@ -3,8 +3,12 @@
module Ci
class DropPipelineWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
+ tags :exclude_from_kubernetes
+
idempotent!
def perform(pipeline_id, failure_reason)
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index f59726c87fb..4dace43298d 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -3,6 +3,8 @@
module Ci
class InitialPipelineProcessWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
index d5e097dc2b5..bd061b5f988 100644
--- a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
+++ b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb
@@ -3,9 +3,12 @@ module Ci
module MergeRequests
class AddTodoWhenBuildFailsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
urgency :low
+ tags :exclude_from_kubernetes
idempotent!
def perform(job_id)
@@ -14,7 +17,7 @@ module Ci
return unless job && project
- ::MergeRequests::AddTodoWhenBuildFailsService.new(job.project, nil).execute(job)
+ ::MergeRequests::AddTodoWhenBuildFailsService.new(project: job.project).execute(job)
end
end
end
diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
index 4de56f54f44..dd7bfff4eb1 100644
--- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
@@ -4,9 +4,12 @@ module Ci
module PipelineArtifacts
class CoverageReportWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
feature_category :code_testing
+ tags :exclude_from_kubernetes
idempotent!
diff --git a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
index 810106e8d9c..558153c69b2 100644
--- a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
@@ -5,14 +5,17 @@ module Ci
class CreateQualityReportWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :pipeline_background
feature_category :code_testing
+ tags :exclude_from_kubernetes
idempotent!
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new.execute(pipeline)
+ Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService.new(pipeline).execute
end
end
end
diff --git a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
index fff979d95a9..004c1d444a2 100644
--- a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
@@ -4,6 +4,8 @@ module Ci
module PipelineArtifacts
class ExpireArtifactsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
@@ -12,6 +14,7 @@ module Ci
deduplicate :until_executed, including_scheduled: true
idempotent!
feature_category :continuous_integration
+ tags :exclude_from_kubernetes
def perform
service = ::Ci::PipelineArtifacts::DestroyAllExpiredService.new
diff --git a/app/workers/ci/pipeline_bridge_status_worker.rb b/app/workers/ci/pipeline_bridge_status_worker.rb
index 3f92f4561e0..3630331b41d 100644
--- a/app/workers/ci/pipeline_bridge_status_worker.rb
+++ b/app/workers/ci/pipeline_bridge_status_worker.rb
@@ -5,6 +5,7 @@ module Ci
include ::ApplicationWorker
include ::PipelineQueue
+ sidekiq_options retry: 3
urgency :high
worker_resource_boundary :cpu
diff --git a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
index bc31876aa1d..b0921f6e10b 100644
--- a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
@@ -3,6 +3,8 @@
module Ci
class PipelineSuccessUnlockArtifactsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
idempotent!
diff --git a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
index aaa77efbb74..d20c501100e 100644
--- a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
+++ b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
@@ -3,6 +3,8 @@
module Ci
class RefDeleteUnlockArtifactsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
idempotent!
diff --git a/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
index 8063e34a1b8..15ed89fd00e 100644
--- a/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
+++ b/app/workers/ci/resource_groups/assign_resource_from_resource_group_worker.rb
@@ -4,6 +4,8 @@ module Ci
module ResourceGroups
class AssignResourceFromResourceGroupWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/ci/retry_pipeline_worker.rb b/app/workers/ci/retry_pipeline_worker.rb
new file mode 100644
index 00000000000..7a1906b3ef9
--- /dev/null
+++ b/app/workers/ci/retry_pipeline_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class RetryPipelineWorker # rubocop:disable Scalability/IdempotentWorker
+ include ::ApplicationWorker
+ include ::PipelineQueue
+
+ urgency :high
+ worker_resource_boundary :cpu
+
+ def perform(pipeline_id, user_id)
+ ::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ ::User.find_by_id(user_id).try do |user|
+ pipeline.retry_failed(user)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/schedule_delete_objects_cron_worker.rb b/app/workers/ci/schedule_delete_objects_cron_worker.rb
index fa0b15deb56..6489665fafd 100644
--- a/app/workers/ci/schedule_delete_objects_cron_worker.rb
+++ b/app/workers/ci/schedule_delete_objects_cron_worker.rb
@@ -3,12 +3,15 @@
module Ci
class ScheduleDeleteObjectsCronWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :continuous_integration
+ tags :exclude_from_kubernetes
idempotent!
def perform(*args)
diff --git a/app/workers/ci/test_failure_history_worker.rb b/app/workers/ci/test_failure_history_worker.rb
index e1562cb3836..3937f720788 100644
--- a/app/workers/ci/test_failure_history_worker.rb
+++ b/app/workers/ci/test_failure_history_worker.rb
@@ -3,8 +3,12 @@
module Ci
class TestFailureHistoryWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineBackgroundQueue
+ tags :exclude_from_kubernetes
+
idempotent!
def perform(pipeline_id)
diff --git a/app/workers/ci_platform_metrics_update_cron_worker.rb b/app/workers/ci_platform_metrics_update_cron_worker.rb
index ec1fc26fad3..05af0a0a73b 100644
--- a/app/workers/ci_platform_metrics_update_cron_worker.rb
+++ b/app/workers/ci_platform_metrics_update_cron_worker.rb
@@ -3,6 +3,8 @@
class CiPlatformMetricsUpdateCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
# This worker does not perform work scoped to a context
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
index 1cac2858156..a8de8efbce6 100644
--- a/app/workers/cleanup_container_repository_worker.rb
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -3,6 +3,8 @@
class CleanupContainerRepositoryWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :container_repository
feature_category :container_registry
urgency :low
diff --git a/app/workers/cluster_configure_istio_worker.rb b/app/workers/cluster_configure_istio_worker.rb
index ec6bdfbd6b6..07c032da838 100644
--- a/app/workers/cluster_configure_istio_worker.rb
+++ b/app/workers/cluster_configure_istio_worker.rb
@@ -2,6 +2,8 @@
class ClusterConfigureIstioWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
worker_has_external_dependencies!
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index f3da4d5c4bb..71374de19f5 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -2,6 +2,8 @@
class ClusterInstallAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_patch_app_worker.rb b/app/workers/cluster_patch_app_worker.rb
index b0393809802..674a565f7f7 100644
--- a/app/workers/cluster_patch_app_worker.rb
+++ b/app/workers/cluster_patch_app_worker.rb
@@ -2,6 +2,8 @@
class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index cb750f3021e..142ad84f746 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -2,6 +2,8 @@
class ClusterProvisionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
worker_has_external_dependencies!
diff --git a/app/workers/cluster_update_app_worker.rb b/app/workers/cluster_update_app_worker.rb
index 29feb813043..dc57a1a90d9 100644
--- a/app/workers/cluster_update_app_worker.rb
+++ b/app/workers/cluster_update_app_worker.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+# Deprecated, to be removed in %14.0 as part of https://gitlab.com/groups/gitlab-org/-/epics/4280
class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
UpdateAlreadyInProgressError = Class.new(StandardError)
@@ -35,6 +36,7 @@ class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: enable CodeReuse/ActiveRecord
def update_prometheus(app, scheduled_time, project)
+ return unless app.managed_prometheus?
return if app.updated_since?(scheduled_time)
return if app.update_in_progress?
diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb
index d4650ab3a85..909ada2044f 100644
--- a/app/workers/cluster_upgrade_app_worker.rb
+++ b/app/workers/cluster_upgrade_app_worker.rb
@@ -2,6 +2,8 @@
class ClusterUpgradeAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
index 4bc29807ea4..19e33cd17b0 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -2,6 +2,8 @@
class ClusterWaitForAppInstallationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_wait_for_app_update_worker.rb b/app/workers/cluster_wait_for_app_update_worker.rb
index c0a11eb93a7..185959884a1 100644
--- a/app/workers/cluster_wait_for_app_update_worker.rb
+++ b/app/workers/cluster_wait_for_app_update_worker.rb
@@ -2,6 +2,8 @@
class ClusterWaitForAppUpdateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
index fa46135d279..4a010c749a2 100644
--- a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -2,6 +2,8 @@
class ClusterWaitForIngressIpAddressWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/clusters/applications/activate_service_worker.rb b/app/workers/clusters/applications/activate_service_worker.rb
index c92f978a7d2..d4d0ae96e03 100644
--- a/app/workers/clusters/applications/activate_service_worker.rb
+++ b/app/workers/clusters/applications/activate_service_worker.rb
@@ -4,6 +4,8 @@ module Clusters
module Applications
class ActivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
loggable_arguments 1
diff --git a/app/workers/clusters/applications/check_prometheus_health_worker.rb b/app/workers/clusters/applications/check_prometheus_health_worker.rb
index cf9534c9a78..4db7314cbc0 100644
--- a/app/workers/clusters/applications/check_prometheus_health_worker.rb
+++ b/app/workers/clusters/applications/check_prometheus_health_worker.rb
@@ -4,6 +4,8 @@ module Clusters
module Applications
class CheckPrometheusHealthWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/clusters/applications/deactivate_service_worker.rb b/app/workers/clusters/applications/deactivate_service_worker.rb
index 4d103bb0edc..935b455a4fc 100644
--- a/app/workers/clusters/applications/deactivate_service_worker.rb
+++ b/app/workers/clusters/applications/deactivate_service_worker.rb
@@ -4,6 +4,8 @@ module Clusters
module Applications
class DeactivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
loggable_arguments 1
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
index a9307931b59..3a4564ca7ab 100644
--- a/app/workers/clusters/applications/uninstall_worker.rb
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -4,6 +4,8 @@ module Clusters
module Applications
class UninstallWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
index dc842788374..18801ad7e64 100644
--- a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
+++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
@@ -4,6 +4,8 @@ module Clusters
module Applications
class WaitForUninstallAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
include ClusterApplications
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 0de26e27631..843be4896a3 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -16,6 +16,7 @@ module ApplicationWorker
included do
set_queue
+ after_set_class_attribute { set_queue }
def structured_payload(payload = {})
context = Gitlab::ApplicationContext.current.merge(
@@ -47,22 +48,14 @@ module ApplicationWorker
class_methods do
def inherited(subclass)
subclass.set_queue
+ subclass.after_set_class_attribute { subclass.set_queue }
end
def set_queue
- queue_name = [queue_namespace, base_queue_name].compact.join(':')
-
+ queue_name = ::Gitlab::SidekiqConfig::WorkerRouter.global.route(self)
sidekiq_options queue: queue_name # rubocop:disable Cop/SidekiqOptionsQueue
end
- def base_queue_name
- name
- .sub(/\AGitlab::/, '')
- .sub(/Worker\z/, '')
- .underscore
- .tr('/', '_')
- end
-
def queue_namespace(new_namespace = nil)
if new_namespace
sidekiq_options queue_namespace: new_namespace
diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb
index a9c557f0175..2ccd55157c6 100644
--- a/app/workers/concerns/chaos_queue.rb
+++ b/app/workers/concerns/chaos_queue.rb
@@ -6,5 +6,6 @@ module ChaosQueue
included do
queue_namespace :chaos
feature_category_not_owned!
+ tags :exclude_from_gitlab_com
end
end
diff --git a/app/workers/concerns/git_garbage_collect_methods.rb b/app/workers/concerns/git_garbage_collect_methods.rb
index 17a80d1ddb3..c46deeb716f 100644
--- a/app/workers/concerns/git_garbage_collect_methods.rb
+++ b/app/workers/concerns/git_garbage_collect_methods.rb
@@ -97,10 +97,10 @@ module GitGarbageCollectMethods
end
rescue GRPC::NotFound => e
Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found")
- raise Gitlab::Git::Repository::NoRepository.new(e)
+ raise Gitlab::Git::Repository::NoRepository, e
rescue GRPC::BadStatus => e
Gitlab::GitLogger.error("#{__method__} failed:\n#{e}")
- raise Gitlab::Git::CommandError.new(e)
+ raise Gitlab::Git::CommandError, e
end
def get_gitaly_client(task, repository)
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 575cd4862b0..6ebf7c7c263 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -9,6 +9,8 @@ module Gitlab
included do
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include ReschedulingMethods
include Gitlab::NotifyUponDeath
@@ -25,15 +27,19 @@ module Gitlab
# client - An instance of `Gitlab::GithubImport::Client`
# hash - A Hash containing the details of the object to import.
def import(project, client, hash)
+ object = representation_class.from_json_hash(hash)
+
+ # To better express in the logs what object is being imported.
+ self.github_id = object.attributes.fetch(:github_id)
+
info(project.id, message: 'starting importer')
- object = representation_class.from_json_hash(hash)
importer_class.new(object, project, client).execute
counter.increment
info(project.id, message: 'importer finished')
- rescue => e
- error(project.id, e)
+ rescue StandardError => e
+ error(project.id, e, hash)
end
def counter
@@ -63,16 +69,19 @@ module Gitlab
private
+ attr_accessor :github_id
+
def info(project_id, extra = {})
logger.info(log_attributes(project_id, extra))
end
- def error(project_id, exception)
+ def error(project_id, exception, data = {})
logger.error(
log_attributes(
project_id,
message: 'importer failed',
- 'error.message': exception.message
+ 'error.message': exception.message,
+ 'github.data': data
)
)
@@ -86,7 +95,8 @@ module Gitlab
extra.merge(
import_source: :github,
project_id: project_id,
- importer: importer_class.name
+ importer: importer_class.name,
+ github_id: github_id
)
end
end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index e5985fb94da..916b273a28f 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -14,7 +14,7 @@ module Gitlab
try_import(client, project)
info(project_id, message: 'stage finished')
- rescue => e
+ rescue StandardError => e
error(project_id, e)
end
diff --git a/app/workers/concerns/gitlab/jira_import/import_worker.rb b/app/workers/concerns/gitlab/jira_import/import_worker.rb
index fdc6e64bbaa..107b6e2e9be 100644
--- a/app/workers/concerns/gitlab/jira_import/import_worker.rb
+++ b/app/workers/concerns/gitlab/jira_import/import_worker.rb
@@ -7,6 +7,8 @@ module Gitlab
included do
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ProjectImportOptions
include Gitlab::JiraImport::QueueOptions
end
diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb
index 96b6e1a2024..47b13cd5bf6 100644
--- a/app/workers/concerns/limited_capacity/job_tracker.rb
+++ b/app/workers/concerns/limited_capacity/job_tracker.rb
@@ -3,21 +3,30 @@ module LimitedCapacity
class JobTracker # rubocop:disable Scalability/IdempotentWorker
include Gitlab::Utils::StrongMemoize
+ LUA_REGISTER_SCRIPT = <<~EOS
+ local set_key, element, max_elements = KEYS[1], ARGV[1], ARGV[2]
+
+ if redis.call("scard", set_key) < tonumber(max_elements) then
+ redis.call("sadd", set_key, element)
+ return true
+ end
+
+ return false
+ EOS
+
def initialize(namespace)
@namespace = namespace
end
- def register(jid)
- _added, @count = with_redis_pipeline do |redis|
- register_job_keys(redis, jid)
- get_job_count(redis)
- end
+ def register(jid, max_jids)
+ with_redis do |redis|
+ redis.eval(LUA_REGISTER_SCRIPT, keys: [counter_key], argv: [jid, max_jids])
+ end.present?
end
def remove(jid)
- _removed, @count = with_redis_pipeline do |redis|
+ with_redis do |redis|
remove_job_keys(redis, jid)
- get_job_count(redis)
end
end
@@ -25,14 +34,13 @@ module LimitedCapacity
completed_jids = Gitlab::SidekiqStatus.completed_jids(running_jids)
return unless completed_jids.any?
- _removed, @count = with_redis_pipeline do |redis|
+ with_redis do |redis|
remove_job_keys(redis, completed_jids)
- get_job_count(redis)
end
end
def count
- @count ||= with_redis { |redis| get_job_count(redis) }
+ with_redis { |redis| redis.scard(counter_key) }
end
def running_jids
@@ -49,14 +57,6 @@ module LimitedCapacity
"worker:#{namespace.to_s.underscore}:running"
end
- def get_job_count(redis)
- redis.scard(counter_key)
- end
-
- def register_job_keys(redis, keys)
- redis.sadd(counter_key, keys)
- end
-
def remove_job_keys(redis, keys)
redis.srem(counter_key, keys)
end
@@ -64,11 +64,5 @@ module LimitedCapacity
def with_redis(&block)
Gitlab::Redis::Queues.with(&block) # rubocop: disable CodeReuse/ActiveRecord
end
-
- def with_redis_pipeline(&block)
- with_redis do |redis|
- redis.pipelined(&block)
- end
- end
end
end
diff --git a/app/workers/concerns/limited_capacity/worker.rb b/app/workers/concerns/limited_capacity/worker.rb
index 9dd8d942146..b4cdfda680f 100644
--- a/app/workers/concerns/limited_capacity/worker.rb
+++ b/app/workers/concerns/limited_capacity/worker.rb
@@ -55,26 +55,14 @@ module LimitedCapacity
def perform_with_capacity(*args)
worker = self.new
worker.remove_failed_jobs
- worker.report_prometheus_metrics(*args)
- required_jobs_count = worker.required_jobs_count(*args)
- arguments = Array.new(required_jobs_count) { args }
+ arguments = Array.new(worker.max_running_jobs) { args }
self.bulk_perform_async(arguments) # rubocop:disable Scalability/BulkPerformWithContext
end
end
def perform(*args)
- return unless has_capacity?
-
- job_tracker.register(jid)
- report_running_jobs_metrics
- perform_work(*args)
- rescue => exception
- raise
- ensure
- job_tracker.remove(jid)
- report_prometheus_metrics(*args)
- re_enqueue(*args) unless exception
+ perform_registered(*args) if job_tracker.register(jid, max_running_jobs)
end
def perform_work(*args)
@@ -89,43 +77,32 @@ module LimitedCapacity
raise NotImplementedError
end
- def has_capacity?
- remaining_capacity > 0
- end
-
- def remaining_capacity
- [
- max_running_jobs - running_jobs_count - self.class.queue_size,
- 0
- ].max
- end
-
- def has_work?(*args)
- remaining_work_count(*args) > 0
- end
-
def remove_failed_jobs
job_tracker.clean_up
end
def report_prometheus_metrics(*args)
report_running_jobs_metrics
- remaining_work_gauge.set(prometheus_labels, remaining_work_count(*args))
- max_running_jobs_gauge.set(prometheus_labels, max_running_jobs)
+ set_metric(:remaining_work_gauge, remaining_work_count(*args))
+ set_metric(:max_running_jobs_gauge, max_running_jobs)
end
- def report_running_jobs_metrics
- running_jobs_gauge.set(prometheus_labels, running_jobs_count)
- end
+ private
- def required_jobs_count(*args)
- [
- remaining_work_count(*args),
- remaining_capacity
- ].min
+ def perform_registered(*args)
+ report_running_jobs_metrics
+ perform_work(*args)
+ rescue StandardError => exception
+ raise
+ ensure
+ job_tracker.remove(jid)
+ report_prometheus_metrics(*args)
+ re_enqueue(*args) unless exception
end
- private
+ def report_running_jobs_metrics
+ set_metric(:running_jobs_gauge, running_jobs_count)
+ end
def running_jobs_count
job_tracker.count
@@ -138,32 +115,21 @@ module LimitedCapacity
end
def re_enqueue(*args)
- return unless has_capacity?
- return unless has_work?(*args)
+ return unless remaining_work_count(*args) > 0
self.class.perform_async(*args)
end
- def running_jobs_gauge
- strong_memoize(:running_jobs_gauge) do
- Gitlab::Metrics.gauge(:limited_capacity_worker_running_jobs, 'Number of running jobs')
- end
- end
-
- def max_running_jobs_gauge
- strong_memoize(:max_running_jobs_gauge) do
- Gitlab::Metrics.gauge(:limited_capacity_worker_max_running_jobs, 'Maximum number of running jobs')
+ def set_metric(name, value)
+ metrics = strong_memoize(:metrics) do
+ {
+ running_jobs_gauge: Gitlab::Metrics.gauge(:limited_capacity_worker_running_jobs, 'Number of running jobs'),
+ max_running_jobs_gauge: Gitlab::Metrics.gauge(:limited_capacity_worker_max_running_jobs, 'Maximum number of running jobs'),
+ remaining_work_gauge: Gitlab::Metrics.gauge(:limited_capacity_worker_remaining_work_count, 'Number of jobs waiting to be enqueued')
+ }
end
- end
-
- def remaining_work_gauge
- strong_memoize(:remaining_work_gauge) do
- Gitlab::Metrics.gauge(:limited_capacity_worker_remaining_work_count, 'Number of jobs waiting to be enqueued')
- end
- end
- def prometheus_labels
- { worker: self.class.name }
+ metrics[name].set({ worker: self.class.name }, value)
end
end
end
diff --git a/app/workers/concerns/reactive_cacheable_worker.rb b/app/workers/concerns/reactive_cacheable_worker.rb
index 9e882c8ac7a..78fcf8087c2 100644
--- a/app/workers/concerns/reactive_cacheable_worker.rb
+++ b/app/workers/concerns/reactive_cacheable_worker.rb
@@ -6,6 +6,8 @@ module ReactiveCacheableWorker
included do
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category_not_owned!
loggable_arguments 0
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
index c3abcdafcf2..e62bd8d9885 100644
--- a/app/workers/concerns/waitable_worker.rb
+++ b/app/workers/concerns/waitable_worker.rb
@@ -33,7 +33,7 @@ module WaitableWorker
args_list.each do |args|
new.perform(*args)
- rescue
+ rescue StandardError
failed << args
end
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 6f99fd089ac..6dee9402691 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -36,13 +36,13 @@ module WorkerAttributes
def feature_category(value, *extras)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
- class_attributes[:feature_category] = value
+ set_class_attribute(:feature_category, value)
end
# Special case: mark this work as not associated with a feature category
# this should be used for cross-cutting concerns, such as mailer workers.
def feature_category_not_owned!
- class_attributes[:feature_category] = :not_owned
+ set_class_attribute(:feature_category, :not_owned)
end
def get_feature_category
@@ -64,7 +64,7 @@ module WorkerAttributes
def urgency(urgency)
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
- class_attributes[:urgency] = urgency
+ set_class_attribute(:urgency, urgency)
end
def get_urgency
@@ -75,8 +75,8 @@ module WorkerAttributes
raise ArgumentError, "Invalid data consistency: #{data_consistency}" unless VALID_DATA_CONSISTENCIES.include?(data_consistency)
raise ArgumentError, 'Data consistency is already set' if class_attributes[:data_consistency]
- class_attributes[:data_consistency_feature_flag] = feature_flag if feature_flag
- class_attributes[:data_consistency] = data_consistency
+ set_class_attribute(:data_consistency_feature_flag, feature_flag) if feature_flag
+ set_class_attribute(:data_consistency, data_consistency)
validate_worker_attributes!
end
@@ -105,7 +105,7 @@ module WorkerAttributes
# doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for
# details
def worker_has_external_dependencies!
- class_attributes[:external_dependencies] = true
+ set_class_attribute(:external_dependencies, true)
end
# Returns a truthy value if the worker has external dependencies.
@@ -118,7 +118,7 @@ module WorkerAttributes
def worker_resource_boundary(boundary)
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
- class_attributes[:resource_boundary] = boundary
+ set_class_attribute(:resource_boundary, boundary)
end
def get_worker_resource_boundary
@@ -126,7 +126,7 @@ module WorkerAttributes
end
def idempotent!
- class_attributes[:idempotent] = true
+ set_class_attribute(:idempotent, true)
validate_worker_attributes!
end
@@ -136,7 +136,7 @@ module WorkerAttributes
end
def weight(value)
- class_attributes[:weight] = value
+ set_class_attribute(:weight, value)
end
def get_weight
@@ -146,7 +146,7 @@ module WorkerAttributes
end
def tags(*values)
- class_attributes[:tags] = values
+ set_class_attribute(:tags, values)
end
def get_tags
@@ -154,8 +154,8 @@ module WorkerAttributes
end
def deduplicate(strategy, options = {})
- class_attributes[:deduplication_strategy] = strategy
- class_attributes[:deduplication_options] = options
+ set_class_attribute(:deduplication_strategy, strategy)
+ set_class_attribute(:deduplication_options, options)
end
def get_deduplicate_strategy
@@ -168,7 +168,7 @@ module WorkerAttributes
end
def big_payload!
- class_attributes[:big_payload] = true
+ set_class_attribute(:big_payload, true)
end
def big_payload?
diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
index 53220a7afed..40cc233307a 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -3,11 +3,14 @@
module ContainerExpirationPolicies
class CleanupContainerRepositoryWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include LimitedCapacity::Worker
include Gitlab::Utils::StrongMemoize
queue_namespace :container_repository
feature_category :container_registry
+ tags :exclude_from_kubernetes
urgency :low
worker_resource_boundary :unknown
idempotent!
@@ -28,7 +31,7 @@ module ContainerExpirationPolicies
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
log_extra_metadata_on_done(:project_id, project.id)
- unless allowed_to_run?(container_repository)
+ unless allowed_to_run?
container_repository.cleanup_unscheduled!
log_extra_metadata_on_done(:cleanup_status, :skipped)
return
@@ -39,9 +42,13 @@ module ContainerExpirationPolicies
log_on_done(result)
end
+ def max_running_jobs
+ return 0 unless throttling_enabled?
+
+ ::Gitlab::CurrentSettings.container_registry_expiration_policies_worker_capacity
+ end
+
def remaining_work_count
- cleanup_scheduled_count = ContainerRepository.cleanup_scheduled.count
- cleanup_unfinished_count = ContainerRepository.cleanup_unfinished.count
total_count = cleanup_scheduled_count + cleanup_unfinished_count
log_info(
@@ -53,50 +60,95 @@ module ContainerExpirationPolicies
total_count
end
- def max_running_jobs
- return 0 unless throttling_enabled?
+ private
- ::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_worker_capacity
- end
+ def container_repository
+ strong_memoize(:container_repository) do
+ ContainerRepository.transaction do
+ # rubocop: disable CodeReuse/ActiveRecord
+ # We need a lock to prevent two workers from picking up the same row
+ container_repository = if loopless_enabled?
+ next_container_repository
+ else
+ ContainerRepository.waiting_for_cleanup
+ .order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
+ .limit(1)
+ .lock('FOR UPDATE SKIP LOCKED')
+ .first
+ end
- private
+ # rubocop: enable CodeReuse/ActiveRecord
+ container_repository&.tap(&:cleanup_ongoing!)
+ end
+ end
+ end
- def allowed_to_run?(container_repository)
- return false unless policy&.enabled && policy&.next_run_at
+ def next_container_repository
+ # rubocop: disable CodeReuse/ActiveRecord
+ next_one_requiring = ContainerRepository.requiring_cleanup
+ .order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
+ .limit(1)
+ .lock('FOR UPDATE SKIP LOCKED')
+ .first
+ return next_one_requiring if next_one_requiring
+
+ ContainerRepository.with_unfinished_cleanup
+ .order(:expiration_policy_started_at)
+ .limit(1)
+ .lock('FOR UPDATE SKIP LOCKED')
+ .first
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
- Time.zone.now + max_cleanup_execution_time.seconds < policy.next_run_at
+ def cleanup_scheduled_count
+ strong_memoize(:cleanup_scheduled_count) do
+ if loopless_enabled?
+ limit = max_running_jobs + 1
+ ContainerExpirationPolicy.with_container_repositories
+ .runnable_schedules
+ .limit(limit)
+ .count
+ else
+ ContainerRepository.cleanup_scheduled.count
+ end
+ end
end
- def throttling_enabled?
- Feature.enabled?(:container_registry_expiration_policies_throttling)
+ def cleanup_unfinished_count
+ strong_memoize(:cleanup_unfinished_count) do
+ if loopless_enabled?
+ limit = max_running_jobs + 1
+ ContainerRepository.with_unfinished_cleanup
+ .limit(limit)
+ .count
+ else
+ ContainerRepository.cleanup_unfinished.count
+ end
+ end
end
- def max_cleanup_execution_time
- ::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
+ def allowed_to_run?
+ return false unless policy&.enabled && policy&.next_run_at
+
+ now = Time.zone.now
+
+ if loopless_enabled?
+ policy.next_run_at < now || (now + max_cleanup_execution_time.seconds < policy.next_run_at)
+ else
+ now + max_cleanup_execution_time.seconds < policy.next_run_at
+ end
end
- def policy
- project.container_expiration_policy
+ def throttling_enabled?
+ Feature.enabled?(:container_registry_expiration_policies_throttling)
end
- def project
- container_repository.project
+ def loopless_enabled?
+ Feature.enabled?(:container_registry_expiration_policies_loopless)
end
- def container_repository
- strong_memoize(:container_repository) do
- ContainerRepository.transaction do
- # rubocop: disable CodeReuse/ActiveRecord
- # We need a lock to prevent two workers from picking up the same row
- container_repository = ContainerRepository.waiting_for_cleanup
- .order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
- .limit(1)
- .lock('FOR UPDATE SKIP LOCKED')
- .first
- # rubocop: enable CodeReuse/ActiveRecord
- container_repository&.tap(&:cleanup_ongoing!)
- end
- end
+ def max_cleanup_execution_time
+ ::Gitlab::CurrentSettings.container_registry_delete_tags_service_timeout
end
def log_info(extra_structure)
@@ -104,6 +156,11 @@ module ContainerExpirationPolicies
end
def log_on_done(result)
+ if result.error?
+ log_extra_metadata_on_done(:cleanup_status, :error)
+ log_extra_metadata_on_done(:cleanup_error_message, result.message)
+ end
+
LOG_ON_DONE_FIELDS.each do |field|
value = result.payload[field]
@@ -120,5 +177,13 @@ module ContainerExpirationPolicies
log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated)
log_extra_metadata_on_done(:running_jobs_count, running_jobs_count)
end
+
+ def policy
+ project.container_expiration_policy
+ end
+
+ def project
+ container_repository.project
+ end
end
end
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index 5ca89179099..dec13485d13 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -2,6 +2,8 @@
class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
include ExclusiveLeaseGuard
diff --git a/app/workers/create_commit_signature_worker.rb b/app/workers/create_commit_signature_worker.rb
index f81baf20d19..0ba2cc41e99 100644
--- a/app/workers/create_commit_signature_worker.rb
+++ b/app/workers/create_commit_signature_worker.rb
@@ -3,6 +3,8 @@
class CreateCommitSignatureWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
weight 2
idempotent!
@@ -36,7 +38,7 @@ class CreateCommitSignatureWorker
# This calculates and caches the signature in the database
commits.each do |commit|
commit&.signature
- rescue => e
+ rescue StandardError => e
Gitlab::AppLogger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}")
end
end
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
index 06790cc89d9..0af203fc3bd 100644
--- a/app/workers/create_note_diff_file_worker.rb
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -3,6 +3,8 @@
class CreateNoteDiffFileWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
def perform(diff_note_id)
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index 68fe44d01ce..a9072e1661f 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -2,6 +2,8 @@
class CreatePipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_creation
diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb
index de274d58ad7..5a326a351e8 100644
--- a/app/workers/database/batched_background_migration_worker.rb
+++ b/app/workers/database/batched_background_migration_worker.rb
@@ -3,9 +3,12 @@
module Database
class BatchedBackgroundMigrationWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :database
+ tags :exclude_from_kubernetes
idempotent!
LEASE_TIMEOUT_MULTIPLIER = 3
@@ -13,7 +16,7 @@ module Database
INTERVAL_VARIANCE = 5.seconds.freeze
def perform
- return unless Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops) && active_migration
+ return unless Feature.enabled?(:execute_batched_migrations_on_schedule, type: :ops, default_enabled: :yaml) && active_migration
with_exclusive_lease(active_migration.interval) do
# Now that we have the exclusive lease, reload migration in case another process has changed it.
@@ -38,7 +41,7 @@ module Database
end
def with_exclusive_lease(interval)
- timeout = max(interval * LEASE_TIMEOUT_MULTIPLIER, MINIMUM_LEASE_TIMEOUT)
+ timeout = [interval * LEASE_TIMEOUT_MULTIPLIER, MINIMUM_LEASE_TIMEOUT].max
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: timeout)
yield if lease.try_obtain
@@ -46,10 +49,6 @@ module Database
lease&.cancel
end
- def max(left, right)
- left >= right ? left : right
- end
-
def lease_key
self.class.name.demodulize.underscore
end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index dbfc273a5ce..f39f8bf44a4 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -2,6 +2,8 @@
class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExclusiveLeaseGuard
queue_namespace :container_repository
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
index 289df8873ec..46dac5d8d39 100644
--- a/app/workers/delete_diff_files_worker.rb
+++ b/app/workers/delete_diff_files_worker.rb
@@ -3,6 +3,8 @@
class DeleteDiffFilesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index 8d7026e2d1e..c7e1a4da965 100644
--- a/app/workers/delete_merged_branches_worker.rb
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -3,6 +3,8 @@
class DeleteMergedBranchesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
def perform(project_id, user_id)
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
index 9cf5631b7d8..75113b4787c 100644
--- a/app/workers/delete_stored_files_worker.rb
+++ b/app/workers/delete_stored_files_worker.rb
@@ -3,6 +3,8 @@
class DeleteStoredFilesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category_not_owned!
loggable_arguments 0
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index ed2e00f1241..f1b9f859ce6 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -3,6 +3,8 @@
class DeleteUserWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :authentication_and_authorization
loggable_arguments 2
diff --git a/app/workers/deployments/drop_older_deployments_worker.rb b/app/workers/deployments/drop_older_deployments_worker.rb
index d6cd92c1da4..6ca819e7942 100644
--- a/app/workers/deployments/drop_older_deployments_worker.rb
+++ b/app/workers/deployments/drop_older_deployments_worker.rb
@@ -4,8 +4,11 @@ module Deployments
class DropOlderDeploymentsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
feature_category :continuous_delivery
+ tags :exclude_from_kubernetes
def perform(deployment_id)
Deployments::OlderDeploymentsDropService.new(deployment_id).execute
diff --git a/app/workers/deployments/execute_hooks_worker.rb b/app/workers/deployments/execute_hooks_worker.rb
index 6be05232321..3046aa28e20 100644
--- a/app/workers/deployments/execute_hooks_worker.rb
+++ b/app/workers/deployments/execute_hooks_worker.rb
@@ -1,16 +1,19 @@
# frozen_string_literal: true
module Deployments
+ # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/329360
class ExecuteHooksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
feature_category :continuous_delivery
worker_resource_boundary :cpu
def perform(deployment_id)
if (deploy = Deployment.find_by_id(deployment_id))
- deploy.execute_hooks
+ deploy.execute_hooks(Time.current)
end
end
end
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
index 62c886010a3..3de06c381cd 100644
--- a/app/workers/deployments/finished_worker.rb
+++ b/app/workers/deployments/finished_worker.rb
@@ -6,6 +6,8 @@ module Deployments
class FinishedWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
feature_category :continuous_delivery
worker_resource_boundary :cpu
@@ -13,7 +15,7 @@ module Deployments
def perform(deployment_id)
if (deploy = Deployment.find_by_id(deployment_id))
LinkMergeRequestsService.new(deploy).execute
- deploy.execute_hooks
+ deploy.execute_hooks(Time.current)
end
end
end
diff --git a/app/workers/deployments/forward_deployment_worker.rb b/app/workers/deployments/forward_deployment_worker.rb
index dd01fcbbafe..946945051ba 100644
--- a/app/workers/deployments/forward_deployment_worker.rb
+++ b/app/workers/deployments/forward_deployment_worker.rb
@@ -6,6 +6,8 @@ module Deployments
class ForwardDeploymentWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
feature_category :continuous_delivery
diff --git a/app/workers/deployments/hooks_worker.rb b/app/workers/deployments/hooks_worker.rb
new file mode 100644
index 00000000000..beac44881fb
--- /dev/null
+++ b/app/workers/deployments/hooks_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Deployments
+ class HooksWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+ feature_category :continuous_delivery
+
+ def perform(params = {})
+ params = params.with_indifferent_access
+
+ if (deploy = Deployment.find_by_id(params[:deployment_id]))
+ deploy.execute_hooks(params[:status_changed_at].to_time)
+ end
+ end
+ end
+end
diff --git a/app/workers/deployments/link_merge_request_worker.rb b/app/workers/deployments/link_merge_request_worker.rb
index 4723691a0bb..70947b3f731 100644
--- a/app/workers/deployments/link_merge_request_worker.rb
+++ b/app/workers/deployments/link_merge_request_worker.rb
@@ -4,6 +4,8 @@ module Deployments
class LinkMergeRequestWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
idempotent!
feature_category :continuous_delivery
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index b72b107985b..eab331433e8 100644
--- a/app/workers/deployments/success_worker.rb
+++ b/app/workers/deployments/success_worker.rb
@@ -6,6 +6,8 @@ module Deployments
class SuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
feature_category :continuous_delivery
worker_resource_boundary :cpu
diff --git a/app/workers/deployments/update_environment_worker.rb b/app/workers/deployments/update_environment_worker.rb
index 2381f9926bc..5c71a13064e 100644
--- a/app/workers/deployments/update_environment_worker.rb
+++ b/app/workers/deployments/update_environment_worker.rb
@@ -4,6 +4,8 @@ module Deployments
class UpdateEnvironmentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :deployment
idempotent!
feature_category :continuous_delivery
diff --git a/app/workers/design_management/copy_design_collection_worker.rb b/app/workers/design_management/copy_design_collection_worker.rb
index 0a6e23fe9da..28b511c7c27 100644
--- a/app/workers/design_management/copy_design_collection_worker.rb
+++ b/app/workers/design_management/copy_design_collection_worker.rb
@@ -4,7 +4,10 @@ module DesignManagement
class CopyDesignCollectionWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :design_management
+ tags :exclude_from_kubernetes
idempotent!
urgency :low
diff --git a/app/workers/design_management/new_version_worker.rb b/app/workers/design_management/new_version_worker.rb
index 4fbf2067be4..eee96858c34 100644
--- a/app/workers/design_management/new_version_worker.rb
+++ b/app/workers/design_management/new_version_worker.rb
@@ -4,6 +4,8 @@ module DesignManagement
class NewVersionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :design_management
# Declare this worker as memory bound due to
# `GenerateImageVersionsService` resizing designs
diff --git a/app/workers/destroy_pages_deployments_worker.rb b/app/workers/destroy_pages_deployments_worker.rb
index 32b539325c9..edd446628aa 100644
--- a/app/workers/destroy_pages_deployments_worker.rb
+++ b/app/workers/destroy_pages_deployments_worker.rb
@@ -8,6 +8,7 @@ class DestroyPagesDeploymentsWorker
loggable_arguments 0, 1
sidekiq_options retry: 3
feature_category :pages
+ tags :exclude_from_kubernetes
def perform(project_id, last_deployment_id = nil)
project = Project.find_by_id(project_id)
diff --git a/app/workers/disallow_two_factor_for_group_worker.rb b/app/workers/disallow_two_factor_for_group_worker.rb
index b3cc7a44672..3a48e3ab5da 100644
--- a/app/workers/disallow_two_factor_for_group_worker.rb
+++ b/app/workers/disallow_two_factor_for_group_worker.rb
@@ -2,9 +2,12 @@
class DisallowTwoFactorForGroupWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExceptionBacktrace
feature_category :subgroups
+ tags :exclude_from_kubernetes
idempotent!
def perform(group_id)
diff --git a/app/workers/disallow_two_factor_for_subgroups_worker.rb b/app/workers/disallow_two_factor_for_subgroups_worker.rb
index 1ca227030e2..f5b31e0bcf0 100644
--- a/app/workers/disallow_two_factor_for_subgroups_worker.rb
+++ b/app/workers/disallow_two_factor_for_subgroups_worker.rb
@@ -2,11 +2,14 @@
class DisallowTwoFactorForSubgroupsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExceptionBacktrace
INTERVAL = 2.seconds.to_i
feature_category :subgroups
+ tags :exclude_from_kubernetes
idempotent!
def perform(group_id)
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 9ceab9bb878..37ed1001c9d 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -3,25 +3,86 @@
class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :issue_tracking
urgency :high
weight 2
+ attr_accessor :raw
+
def perform(raw)
- return unless Gitlab::IncomingEmail.enabled?
+ return unless should_perform?
- begin
- Gitlab::Email::Receiver.new(raw).execute
- rescue => e
- handle_failure(raw, e)
- end
+ @raw = raw
+ execute_receiver
+ end
+
+ def should_perform?
+ Gitlab::IncomingEmail.enabled?
end
private
- def handle_failure(raw, error)
- Gitlab::AppLogger.warn("Email can not be processed: #{error}\n\n#{raw}")
+ def execute_receiver
+ receiver.execute
+ log_success
+ rescue StandardError => e
+ log_error(e)
+ handle_failure(e)
+ end
+
+ def receiver
+ @receiver ||= Gitlab::Email::Receiver.new(raw)
+ end
+
+ def logger
+ Sidekiq.logger
+ end
+
+ def log_success
+ logger.info(build_message('Successfully processed message', receiver.mail_metadata))
+ end
+
+ def log_error(error)
+ payload =
+ case error
+ # Unparsable e-mails don't have metadata we can use
+ when Gitlab::Email::EmailUnparsableError, Gitlab::Email::EmptyEmailError
+ {}
+ else
+ mail_metadata
+ end
+
+ # We don't need the backtrace and more details if the e-mail couldn't be processed
+ if error.is_a?(Gitlab::Email::ProcessingError)
+ payload['exception.class'] = error.class.name
+ else
+ Gitlab::ExceptionLogFormatter.format!(error, payload)
+ Gitlab::ErrorTracking.track_exception(error)
+ end
+
+ logger.error(build_message('Error processing message', payload))
+ end
+
+ def build_message(message, params = {})
+ {
+ class: self.class.name,
+ Labkit::Correlation::CorrelationId::LOG_KEY => Labkit::Correlation::CorrelationId.current_id,
+ message: message
+ }.merge(params)
+ end
+
+ def mail_metadata
+ receiver.mail_metadata
+ rescue StandardError => e
+ # We should never get here as long as we check EmailUnparsableError, but
+ # let's be defensive in case we did something wrong.
+ Gitlab::ErrorTracking.track_exception(e)
+ {}
+ end
+ def handle_failure(error)
return unless raw.present?
can_retry = false
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 978b65802dd..9c4418c5f31 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -3,6 +3,8 @@
class EmailsOnPushWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
attr_reader :email, :skip_premailer
feature_category :source_code_management
@@ -56,7 +58,7 @@ class EmailsOnPushWorker # rubocop:disable Scalability/IdempotentWorker
end
end
- EmailsOnPushService.valid_recipients(recipients).each do |recipient|
+ Integrations::EmailsOnPush.valid_recipients(recipients).each do |recipient|
send_email(
recipient,
project_id,
diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb
index ada52d3402d..64028839df1 100644
--- a/app/workers/environments/auto_stop_cron_worker.rb
+++ b/app/workers/environments/auto_stop_cron_worker.rb
@@ -3,6 +3,8 @@
module Environments
class AutoStopCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :continuous_delivery
diff --git a/app/workers/environments/canary_ingress/update_worker.rb b/app/workers/environments/canary_ingress/update_worker.rb
index 53cc38e9eec..ecdfc6f0581 100644
--- a/app/workers/environments/canary_ingress/update_worker.rb
+++ b/app/workers/environments/canary_ingress/update_worker.rb
@@ -9,6 +9,7 @@ module Environments
idempotent!
worker_has_external_dependencies!
feature_category :continuous_delivery
+ tags :exclude_from_kubernetes
def perform(environment_id, params)
Environment.find_by_id(environment_id).try do |environment|
diff --git a/app/workers/error_tracking_issue_link_worker.rb b/app/workers/error_tracking_issue_link_worker.rb
index 4ad80d57f6b..6c5a96822a6 100644
--- a/app/workers/error_tracking_issue_link_worker.rb
+++ b/app/workers/error_tracking_issue_link_worker.rb
@@ -7,6 +7,8 @@
# until the prior link is deleted.
class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExclusiveLeaseGuard
include Gitlab::Utils::StrongMemoize
diff --git a/app/workers/experiments/record_conversion_event_worker.rb b/app/workers/experiments/record_conversion_event_worker.rb
index e38ce7b3d01..9fc76a2173b 100644
--- a/app/workers/experiments/record_conversion_event_worker.rb
+++ b/app/workers/experiments/record_conversion_event_worker.rb
@@ -4,7 +4,10 @@ module Experiments
class RecordConversionEventWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :users
+ tags :exclude_from_kubernetes
urgency :low
idempotent!
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 50fdd046491..a9fa94ef301 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -2,6 +2,8 @@
class ExpireBuildArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index e6cd60a3e47..3e6e81867bd 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -3,6 +3,8 @@
class ExpireBuildInstanceArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :continuous_integration
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 48bb1160ae8..074c35997f6 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -2,6 +2,8 @@
class ExpireJobCacheWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_cache
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index cbea46cdccd..3c48c4ba3cd 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -2,6 +2,8 @@
class ExpirePipelineCacheWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_cache
diff --git a/app/workers/export_csv_worker.rb b/app/workers/export_csv_worker.rb
index f2da381a34a..a2ad0cb92fd 100644
--- a/app/workers/export_csv_worker.rb
+++ b/app/workers/export_csv_worker.rb
@@ -3,6 +3,8 @@
class ExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :issue_tracking
worker_resource_boundary :cpu
loggable_arguments 2
diff --git a/app/workers/flush_counter_increments_worker.rb b/app/workers/flush_counter_increments_worker.rb
index b7e3c0c134d..44d30b4ba3d 100644
--- a/app/workers/flush_counter_increments_worker.rb
+++ b/app/workers/flush_counter_increments_worker.rb
@@ -8,7 +8,10 @@
class FlushCounterIncrementsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category_not_owned!
+ tags :exclude_from_kubernetes
urgency :low
deduplicate :until_executing, including_scheduled: true
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
deleted file mode 100644
index a2aab23db7b..00000000000
--- a/app/workers/git_garbage_collect_worker.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-# According to our docs, we can only remove workers on major releases
-# https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#removing-workers.
-#
-# We need to still maintain this until 14.0 but with the current functionality.
-#
-# In https://gitlab.com/gitlab-org/gitlab/-/issues/299290 we track that removal.
-class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- sidekiq_options retry: false
- feature_category :gitaly
- loggable_arguments 1, 2, 3
-
- def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
- ::Projects::GitGarbageCollectWorker.new.perform(project_id, task, lease_key, lease_uuid)
- end
-end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index af406b32415..f25296f0461 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -8,6 +8,8 @@ module Gitlab
# stage.
class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ::Gitlab::Import::AdvanceStage
sidekiq_options dead: false
diff --git a/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb b/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb
index 79ef917bbc5..a8b79cf9b3a 100644
--- a/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb
+++ b/app/workers/gitlab/github_import/import_pull_request_merged_by_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
class ImportPullRequestMergedByWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
+ tags :exclude_from_kubernetes
+
def representation_class
Gitlab::GithubImport::Representation::PullRequest
end
diff --git a/app/workers/gitlab/github_import/import_pull_request_review_worker.rb b/app/workers/gitlab/github_import/import_pull_request_review_worker.rb
index b8516fb8670..5ee88d5d32b 100644
--- a/app/workers/gitlab/github_import/import_pull_request_review_worker.rb
+++ b/app/workers/gitlab/github_import/import_pull_request_review_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
class ImportPullRequestReviewWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
+ tags :exclude_from_kubernetes
+
def representation_class
Gitlab::GithubImport::Representation::PullRequestReview
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 0ddd893d0d1..1c769921ab3 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -4,6 +4,8 @@ module Gitlab
module GithubImport
class RefreshImportJidWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
# The interval to schedule new instances of this job at.
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 058e1a0853d..f5980cc248e 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index 202bb335ca1..7ca23ecad20 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportBaseDataWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index 486057804b4..d66698277b0 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportIssuesAndDiffNotesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
index de2a7f9fc29..2a66a08d534 100644
--- a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index e1da26a9d48..873e389fca6 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportNotesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
index 3e15c346659..5743648680d 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker.rb
@@ -5,9 +5,13 @@ module Gitlab
module Stage
class ImportPullRequestsMergedByWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
+ tags :exclude_from_kubernetes
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
index 790e8b0eccf..532d550f190 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb
@@ -5,9 +5,13 @@ module Gitlab
module Stage
class ImportPullRequestsReviewsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
+ tags :exclude_from_kubernetes
+
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index bf2defa6326..5755aea21ce 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportPullRequestsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
index 3338f7e58c0..e113563ce8b 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class ImportRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
diff --git a/app/workers/gitlab/import/stuck_import_job.rb b/app/workers/gitlab/import/stuck_import_job.rb
index 16be7a77ab1..ac789ce1188 100644
--- a/app/workers/gitlab/import/stuck_import_job.rb
+++ b/app/workers/gitlab/import/stuck_import_job.rb
@@ -9,6 +9,8 @@ module Gitlab
included do
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker updates several import states inline and does not schedule
# other jobs. So no context needed
diff --git a/app/workers/gitlab/jira_import/advance_stage_worker.rb b/app/workers/gitlab/jira_import/advance_stage_worker.rb
index c3a64669c60..6387054d448 100644
--- a/app/workers/gitlab/jira_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/jira_import/advance_stage_worker.rb
@@ -4,6 +4,8 @@ module Gitlab
module JiraImport
class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include QueueOptions
include ::Gitlab::Import::AdvanceStage
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index d1ceda4fd6a..98bde2218c2 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -4,6 +4,8 @@ module Gitlab
module JiraImport
class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include NotifyUponDeath
include Gitlab::JiraImport::QueueOptions
include Gitlab::Import::DatabaseHelpers
@@ -13,7 +15,7 @@ module Gitlab
def perform(project_id, jira_issue_id, issue_attributes, waiter_key)
issue_id = create_issue(issue_attributes, project_id)
JiraImport.cache_issue_mapping(issue_id, jira_issue_id, project_id)
- rescue => ex
+ rescue StandardError => ex
# Todo: Record jira issue id(or better jira issue key),
# so that we can report the list of failed to import issues to the user
# see https://gitlab.com/gitlab-org/gitlab/-/issues/211653
diff --git a/app/workers/gitlab/jira_import/stage/start_import_worker.rb b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
index bfc02224ee4..e327ced8c65 100644
--- a/app/workers/gitlab/jira_import/stage/start_import_worker.rb
+++ b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
module Stage
class StartImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ProjectStartImport
include ProjectImportOptions
include Gitlab::JiraImport::QueueOptions
diff --git a/app/workers/gitlab/phabricator_import/import_tasks_worker.rb b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
index 1b1d7b35dd5..867a12fbac2 100644
--- a/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
+++ b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
@@ -3,6 +3,8 @@ module Gitlab
module PhabricatorImport
class ImportTasksWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ProjectImportOptions # This marks the project as failed after too many tries
def importer_class
diff --git a/app/workers/gitlab_performance_bar_stats_worker.rb b/app/workers/gitlab_performance_bar_stats_worker.rb
index 558df0ab7b3..4f7fdcf96f0 100644
--- a/app/workers/gitlab_performance_bar_stats_worker.rb
+++ b/app/workers/gitlab_performance_bar_stats_worker.rb
@@ -3,6 +3,8 @@
class GitlabPerformanceBarStatsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
LEASE_KEY = 'gitlab:performance_bar_stats'
LEASE_TIMEOUT = 600
WORKER_DELAY = 120
@@ -10,6 +12,7 @@ class GitlabPerformanceBarStatsWorker
STATS_KEY_EXPIRE = 30.minutes.to_i
feature_category :metrics
+ tags :exclude_from_kubernetes
idempotent!
def perform(lease_uuid)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index b8e1e3d8fc4..de1e9af7bae 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -2,6 +2,8 @@
class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include Gitlab::ShellAdapter
feature_category :source_code_management
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 901785f462b..2c140c89e26 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -2,10 +2,12 @@
class GroupDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExceptionBacktrace
feature_category :subgroups
- tags :requires_disk_io
+ tags :requires_disk_io, :exclude_from_kubernetes
def perform(group_id, user_id)
begin
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
index 5cbdfcb0602..735d8a2447a 100644
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -4,8 +4,11 @@ module HashedStorage
class MigratorWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :hashed_storage
feature_category :source_code_management
+ tags :exclude_from_gitlab_com
# @param [Integer] start initial ID of the batch
# @param [Integer] finish last ID of the batch
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
index 03e53058dbb..0659c8a6a46 100644
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -4,8 +4,11 @@ module HashedStorage
class ProjectMigrateWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :hashed_storage
loggable_arguments 1
+ tags :exclude_from_gitlab_com
attr_reader :project_id
diff --git a/app/workers/hashed_storage/project_rollback_worker.rb b/app/workers/hashed_storage/project_rollback_worker.rb
index d4a5e474323..a5ee8b35176 100644
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ b/app/workers/hashed_storage/project_rollback_worker.rb
@@ -4,8 +4,11 @@ module HashedStorage
class ProjectRollbackWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :hashed_storage
loggable_arguments 1
+ tags :exclude_from_gitlab_com
attr_reader :project_id
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
index a220d3b2226..447bdfa6220 100644
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ b/app/workers/hashed_storage/rollbacker_worker.rb
@@ -4,8 +4,11 @@ module HashedStorage
class RollbackerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :hashed_storage
feature_category :source_code_management
+ tags :exclude_from_gitlab_com
# @param [Integer] start initial ID of the batch
# @param [Integer] finish last ID of the batch
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
index dd345434d08..6e112a47932 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -2,6 +2,8 @@
class ImportExportProjectCleanupWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
index 521e5b8fbc2..46b59dc398f 100644
--- a/app/workers/import_issues_csv_worker.rb
+++ b/app/workers/import_issues_csv_worker.rb
@@ -3,6 +3,8 @@
class ImportIssuesCsvWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
feature_category :issue_tracking
worker_resource_boundary :cpu
diff --git a/app/workers/incident_management/add_severity_system_note_worker.rb b/app/workers/incident_management/add_severity_system_note_worker.rb
index 9f132531562..62ed902e488 100644
--- a/app/workers/incident_management/add_severity_system_note_worker.rb
+++ b/app/workers/incident_management/add_severity_system_note_worker.rb
@@ -4,8 +4,11 @@ module IncidentManagement
class AddSeveritySystemNoteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :incident_management
feature_category :incident_management
+ tags :exclude_from_kubernetes
def perform(incident_id, user_id)
return if incident_id.blank? || user_id.blank?
diff --git a/app/workers/incident_management/pager_duty/process_incident_worker.rb b/app/workers/incident_management/pager_duty/process_incident_worker.rb
index 3f378b012a1..413a297a024 100644
--- a/app/workers/incident_management/pager_duty/process_incident_worker.rb
+++ b/app/workers/incident_management/pager_duty/process_incident_worker.rb
@@ -5,6 +5,8 @@ module IncidentManagement
class ProcessIncidentWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :incident_management
feature_category :incident_management
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index 59464b81d1b..3b90e296ad4 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -4,12 +4,20 @@ module IncidentManagement
class ProcessAlertWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :incident_management
feature_category :incident_management
# `project_id` and `alert_payload` are deprecated and can be removed
# starting from 14.0 release
# https://gitlab.com/gitlab-org/gitlab/-/issues/224500
+ #
+ # This worker is not scheduled anymore since
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60285
+ # and will be removed completely via
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/224500
+ # in 14.0.
def perform(_project_id = nil, _alert_payload = nil, alert_id = nil)
return unless alert_id
diff --git a/app/workers/incident_management/process_alert_worker_v2.rb b/app/workers/incident_management/process_alert_worker_v2.rb
new file mode 100644
index 00000000000..04bf6970578
--- /dev/null
+++ b/app/workers/incident_management/process_alert_worker_v2.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class ProcessAlertWorkerV2 # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ queue_namespace :incident_management
+ feature_category :incident_management
+
+ idempotent!
+
+ def perform(alert_id)
+ return unless alert_id
+
+ alert = find_alert(alert_id)
+ return unless alert
+
+ result = create_issue_for(alert)
+ return if result.success?
+
+ log_warning(alert, result)
+ end
+
+ private
+
+ def find_alert(alert_id)
+ AlertManagement::Alert.find_by_id(alert_id)
+ end
+
+ def create_issue_for(alert)
+ AlertManagement::CreateAlertIssueService
+ .new(alert, User.alert_bot)
+ .execute
+ end
+
+ def log_warning(alert, result)
+ issue_id = result.payload[:issue]&.id
+
+ Gitlab::AppLogger.warn(
+ message: 'Cannot process an Incident',
+ issue_id: issue_id,
+ alert_id: alert.id,
+ errors: result.message
+ )
+ end
+ end
+end
diff --git a/app/workers/incident_management/process_prometheus_alert_worker.rb b/app/workers/incident_management/process_prometheus_alert_worker.rb
index 4b778f6a621..7b5c6fd9001 100644
--- a/app/workers/incident_management/process_prometheus_alert_worker.rb
+++ b/app/workers/incident_management/process_prometheus_alert_worker.rb
@@ -4,6 +4,8 @@ module IncidentManagement
class ProcessPrometheusAlertWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :incident_management
feature_category :incident_management
worker_resource_boundary :cpu
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index 1fd959c8763..662817b5a92 100644
--- a/app/workers/invalid_gpg_signature_update_worker.rb
+++ b/app/workers/invalid_gpg_signature_update_worker.rb
@@ -3,6 +3,8 @@
class InvalidGpgSignatureUpdateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
weight 2
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index c5bdb3e0970..4378da186a7 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -6,6 +6,8 @@ require 'socket'
class IrkerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
def perform(project_id, channels, colors, push_data, settings)
diff --git a/app/workers/issuable/label_links_destroy_worker.rb b/app/workers/issuable/label_links_destroy_worker.rb
new file mode 100644
index 00000000000..f663c410fba
--- /dev/null
+++ b/app/workers/issuable/label_links_destroy_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Issuable
+ class LabelLinksDestroyWorker
+ include ApplicationWorker
+
+ idempotent!
+ feature_category :issue_tracking
+
+ def perform(target_id, target_type)
+ ::Issuable::DestroyLabelLinksService.new(target_id, target_type).execute
+ end
+ end
+end
diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb
index eb96a78497c..41facab6bb9 100644
--- a/app/workers/issuable_export_csv_worker.rb
+++ b/app/workers/issuable_export_csv_worker.rb
@@ -3,6 +3,8 @@
class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :issue_tracking
worker_resource_boundary :cpu
loggable_arguments 2
@@ -47,4 +49,4 @@ class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
end
end
-IssuableExportCsvWorker.prepend_if_ee('::EE::IssuableExportCsvWorker')
+IssuableExportCsvWorker.prepend_mod_with('IssuableExportCsvWorker')
diff --git a/app/workers/issuables/clear_groups_issue_counter_worker.rb b/app/workers/issuables/clear_groups_issue_counter_worker.rb
new file mode 100644
index 00000000000..a8d6fd2f870
--- /dev/null
+++ b/app/workers/issuables/clear_groups_issue_counter_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Issuables
+ class ClearGroupsIssueCounterWorker
+ include ApplicationWorker
+
+ idempotent!
+ urgency :low
+ feature_category :issue_tracking
+
+ def perform(group_ids = [])
+ return if group_ids.empty?
+
+ groups_with_ancestors = Gitlab::ObjectHierarchy
+ .new(Group.by_id(group_ids))
+ .base_and_ancestors
+
+ clear_cached_count(groups_with_ancestors)
+ end
+
+ private
+
+ def clear_cached_count(groups)
+ groups.each do |group|
+ Groups::OpenIssuesCountService.new(group).clear_all_cache_keys
+ end
+ end
+ end
+end
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index d735295d046..9077b42d645 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -2,6 +2,8 @@
class IssueDueSchedulerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :issue_tracking
diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb
index 5b547ab0c8d..dba791c3f05 100644
--- a/app/workers/issue_placement_worker.rb
+++ b/app/workers/issue_placement_worker.rb
@@ -3,7 +3,10 @@
class IssuePlacementWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
+ deduplicate :until_executed, including_scheduled: true
feature_category :issue_tracking
urgency :high
worker_resource_boundary :cpu
@@ -17,6 +20,10 @@ class IssuePlacementWorker
issue = find_issue(issue_id, project_id)
return unless issue
+ # Temporary disable moving null elements because of performance problems
+ # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
+ return if issue.blocked_for_repositioning?
+
# Move the oldest 100 unpositioned items to the end.
# This is to deal with out-of-order execution of the worker,
# while preserving creation order.
@@ -30,7 +37,7 @@ class IssuePlacementWorker
leftover = to_place.pop if to_place.count > QUERY_LIMIT
Issue.move_nulls_to_end(to_place)
- Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position))
+ Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
IssuePlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
rescue RelativePositioning::NoSpaceLeft => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)
diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb
index a9ad66198f3..9eac451f107 100644
--- a/app/workers/issue_rebalancing_worker.rb
+++ b/app/workers/issue_rebalancing_worker.rb
@@ -3,14 +3,22 @@
class IssueRebalancingWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
urgency :low
feature_category :issue_tracking
+ tags :exclude_from_kubernetes
def perform(ignore = nil, project_id = nil)
return if project_id.nil?
project = Project.find(project_id)
+
+ # Temporary disable reabalancing for performance reasons
+ # For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
+ return if project.root_namespace&.issue_repositioning_disabled?
+
# All issues are equivalent as far as we are concerned
issue = project.issues.take # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/jira_connect/sync_branch_worker.rb b/app/workers/jira_connect/sync_branch_worker.rb
index 1af51c4bb74..b8211286d1c 100644
--- a/app/workers/jira_connect/sync_branch_worker.rb
+++ b/app/workers/jira_connect/sync_branch_worker.rb
@@ -4,6 +4,8 @@ module JiraConnect
class SyncBranchWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :jira_connect
feature_category :integrations
loggable_arguments 1, 2
diff --git a/app/workers/jira_connect/sync_builds_worker.rb b/app/workers/jira_connect/sync_builds_worker.rb
index 9cb5d5d247d..11a3b598035 100644
--- a/app/workers/jira_connect/sync_builds_worker.rb
+++ b/app/workers/jira_connect/sync_builds_worker.rb
@@ -4,11 +4,14 @@ module JiraConnect
class SyncBuildsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
worker_has_external_dependencies!
queue_namespace :jira_connect
feature_category :integrations
+ tags :exclude_from_kubernetes
def perform(pipeline_id, sequence_id)
pipeline = Ci::Pipeline.find_by_id(pipeline_id)
diff --git a/app/workers/jira_connect/sync_deployments_worker.rb b/app/workers/jira_connect/sync_deployments_worker.rb
index 7272d35f4cb..9f75b1161f0 100644
--- a/app/workers/jira_connect/sync_deployments_worker.rb
+++ b/app/workers/jira_connect/sync_deployments_worker.rb
@@ -4,11 +4,14 @@ module JiraConnect
class SyncDeploymentsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
worker_has_external_dependencies!
queue_namespace :jira_connect
feature_category :integrations
+ tags :exclude_from_kubernetes
def perform(deployment_id, sequence_id)
deployment = Deployment.find_by_id(deployment_id)
diff --git a/app/workers/jira_connect/sync_feature_flags_worker.rb b/app/workers/jira_connect/sync_feature_flags_worker.rb
index 496b9f1626d..0d8d3d3142e 100644
--- a/app/workers/jira_connect/sync_feature_flags_worker.rb
+++ b/app/workers/jira_connect/sync_feature_flags_worker.rb
@@ -4,11 +4,14 @@ module JiraConnect
class SyncFeatureFlagsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
worker_has_external_dependencies!
queue_namespace :jira_connect
feature_category :integrations
+ tags :exclude_from_kubernetes
def perform(feature_flag_id, sequence_id)
feature_flag = ::Operations::FeatureFlag.find_by_id(feature_flag_id)
diff --git a/app/workers/jira_connect/sync_merge_request_worker.rb b/app/workers/jira_connect/sync_merge_request_worker.rb
index 543d8e002fe..6b3a6ae84ad 100644
--- a/app/workers/jira_connect/sync_merge_request_worker.rb
+++ b/app/workers/jira_connect/sync_merge_request_worker.rb
@@ -4,6 +4,8 @@ module JiraConnect
class SyncMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :jira_connect
feature_category :integrations
idempotent!
diff --git a/app/workers/jira_connect/sync_project_worker.rb b/app/workers/jira_connect/sync_project_worker.rb
index 4d52705f207..dfff0c4b3b6 100644
--- a/app/workers/jira_connect/sync_project_worker.rb
+++ b/app/workers/jira_connect/sync_project_worker.rb
@@ -4,8 +4,11 @@ module JiraConnect
class SyncProjectWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :jira_connect
feature_category :integrations
+ tags :exclude_from_kubernetes
idempotent!
worker_has_external_dependencies!
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index 309d3e13477..44f8f1e446c 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -3,6 +3,8 @@
module MailScheduler
class IssueDueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include MailSchedulerQueue
feature_category :issue_tracking
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 309f23c8708..8645cc93511 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -5,6 +5,8 @@ require 'active_job/arguments'
module MailScheduler
class NotificationServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include MailSchedulerQueue
feature_category :issue_tracking
diff --git a/app/workers/member_invitation_reminder_emails_worker.rb b/app/workers/member_invitation_reminder_emails_worker.rb
index 971d6abaa51..bfee8ab1fab 100644
--- a/app/workers/member_invitation_reminder_emails_worker.rb
+++ b/app/workers/member_invitation_reminder_emails_worker.rb
@@ -2,9 +2,12 @@
class MemberInvitationReminderEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
+ tags :exclude_from_kubernetes
urgency :low
def perform
diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb
index 2c17120bf48..0ee5654eaba 100644
--- a/app/workers/members_destroyer/unassign_issuables_worker.rb
+++ b/app/workers/members_destroyer/unassign_issuables_worker.rb
@@ -4,6 +4,8 @@ module MembersDestroyer
class UnassignIssuablesWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
ENTITY_TYPES = %w(Group Project).freeze
queue_namespace :unassign_issuables
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index fbd62ac0a91..162c6dc2a88 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -3,7 +3,10 @@
class MergeRequestCleanupRefsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
+ tags :exclude_from_kubernetes
idempotent!
def perform(merge_request_id)
diff --git a/app/workers/merge_request_mergeability_check_worker.rb b/app/workers/merge_request_mergeability_check_worker.rb
index 70d5f49d70e..13961de1f59 100644
--- a/app/workers/merge_request_mergeability_check_worker.rb
+++ b/app/workers/merge_request_mergeability_check_worker.rb
@@ -3,6 +3,8 @@
class MergeRequestMergeabilityCheckWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
idempotent!
diff --git a/app/workers/merge_requests/assignees_change_worker.rb b/app/workers/merge_requests/assignees_change_worker.rb
index 9865563e357..fe39f20151f 100644
--- a/app/workers/merge_requests/assignees_change_worker.rb
+++ b/app/workers/merge_requests/assignees_change_worker.rb
@@ -3,6 +3,8 @@
class MergeRequests::AssigneesChangeWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
urgency :high
deduplicate :until_executed
@@ -19,7 +21,7 @@ class MergeRequests::AssigneesChangeWorker
return if users.blank?
::MergeRequests::HandleAssigneesChangeService
- .new(merge_request.target_project, current_user)
+ .new(project: merge_request.target_project, current_user: current_user)
.execute(merge_request, users, execute_hooks: true)
rescue ActiveRecord::RecordNotFound
end
diff --git a/app/workers/merge_requests/create_pipeline_worker.rb b/app/workers/merge_requests/create_pipeline_worker.rb
index 244ba1af300..a79a92a5419 100644
--- a/app/workers/merge_requests/create_pipeline_worker.rb
+++ b/app/workers/merge_requests/create_pipeline_worker.rb
@@ -3,6 +3,8 @@
module MergeRequests
class CreatePipelineWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_creation
@@ -21,7 +23,7 @@ module MergeRequests
merge_request = MergeRequest.find_by_id(merge_request_id)
return unless merge_request
- MergeRequests::CreatePipelineService.new(project, user).execute(merge_request)
+ MergeRequests::CreatePipelineService.new(project: project, current_user: user).execute(merge_request)
merge_request.update_head_pipeline
end
end
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index eb83d10af33..1ce3a99b298 100644
--- a/app/workers/merge_requests/delete_source_branch_worker.rb
+++ b/app/workers/merge_requests/delete_source_branch_worker.rb
@@ -3,6 +3,8 @@
class MergeRequests::DeleteSourceBranchWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
urgency :high
idempotent!
@@ -17,7 +19,7 @@ class MergeRequests::DeleteSourceBranchWorker
::Branches::DeleteService.new(merge_request.source_project, user)
.execute(merge_request.source_branch)
- ::MergeRequests::RetargetChainService.new(merge_request.source_project, user)
+ ::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
.execute(merge_request)
rescue ActiveRecord::RecordNotFound
end
diff --git a/app/workers/merge_requests/handle_assignees_change_worker.rb b/app/workers/merge_requests/handle_assignees_change_worker.rb
index e79d8293bae..4c0500cd520 100644
--- a/app/workers/merge_requests/handle_assignees_change_worker.rb
+++ b/app/workers/merge_requests/handle_assignees_change_worker.rb
@@ -3,6 +3,8 @@
class MergeRequests::HandleAssigneesChangeWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
urgency :high
deduplicate :until_executed
@@ -15,7 +17,7 @@ class MergeRequests::HandleAssigneesChangeWorker
old_assignees = User.id_in(old_assignee_ids)
::MergeRequests::HandleAssigneesChangeService
- .new(merge_request.target_project, user)
+ .new(project: merge_request.target_project, current_user: user)
.execute(merge_request, old_assignees, options)
rescue ActiveRecord::RecordNotFound
end
diff --git a/app/workers/merge_requests/resolve_todos_worker.rb b/app/workers/merge_requests/resolve_todos_worker.rb
index 2a5f742f809..8bb88091efe 100644
--- a/app/workers/merge_requests/resolve_todos_worker.rb
+++ b/app/workers/merge_requests/resolve_todos_worker.rb
@@ -3,6 +3,8 @@
class MergeRequests::ResolveTodosWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
urgency :high
deduplicate :until_executed
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 270bd831f96..df5a7a904fc 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -3,17 +3,27 @@
class MergeWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
urgency :high
weight 5
loggable_arguments 2
+ idempotent!
+
+ deduplicate :until_executed, including_scheduled: true
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
- current_user = User.find(current_user_id)
- merge_request = MergeRequest.find(merge_request_id)
- MergeRequests::MergeService.new(merge_request.target_project, current_user, params)
+ begin
+ current_user = User.find(current_user_id)
+ merge_request = MergeRequest.find(merge_request_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ MergeRequests::MergeService.new(project: merge_request.target_project, current_user: current_user, params: params)
.execute(merge_request)
end
end
diff --git a/app/workers/metrics/dashboard/prune_old_annotations_worker.rb b/app/workers/metrics/dashboard/prune_old_annotations_worker.rb
index 2a9ce3bb8e6..5e8067a4438 100644
--- a/app/workers/metrics/dashboard/prune_old_annotations_worker.rb
+++ b/app/workers/metrics/dashboard/prune_old_annotations_worker.rb
@@ -5,6 +5,8 @@ module Metrics
class PruneOldAnnotationsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
DELETE_LIMIT = 10_000
DEFAULT_CUT_OFF_PERIOD = 2.weeks
diff --git a/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb b/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
index cbdd69c6e8c..6f2ff8cca13 100644
--- a/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
+++ b/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
@@ -4,6 +4,8 @@ module Metrics
module Dashboard
class ScheduleAnnotationsPruneWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/metrics/dashboard/sync_dashboards_worker.rb b/app/workers/metrics/dashboard/sync_dashboards_worker.rb
index 7a124a33f9e..0fdc7825f47 100644
--- a/app/workers/metrics/dashboard/sync_dashboards_worker.rb
+++ b/app/workers/metrics/dashboard/sync_dashboards_worker.rb
@@ -5,7 +5,10 @@ module Metrics
class SyncDashboardsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :metrics
+ tags :exclude_from_kubernetes
idempotent!
diff --git a/app/workers/migrate_external_diffs_worker.rb b/app/workers/migrate_external_diffs_worker.rb
index 3ef399bd9fc..a73a9be4f0c 100644
--- a/app/workers/migrate_external_diffs_worker.rb
+++ b/app/workers/migrate_external_diffs_worker.rb
@@ -3,6 +3,8 @@
class MigrateExternalDiffsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
def perform(merge_request_diff_id)
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index 1c8054d8fbd..91cad6f2a5c 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -8,6 +8,8 @@
# namespace. For those use ProjectDestroyWorker instead.
class NamespacelessProjectDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExceptionBacktrace
feature_category :authentication_and_authorization
diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb
index 3070afed3d6..7985325d1ad 100644
--- a/app/workers/namespaces/in_product_marketing_emails_worker.rb
+++ b/app/workers/namespaces/in_product_marketing_emails_worker.rb
@@ -3,9 +3,12 @@
module Namespaces
class InProductMarketingEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
+ tags :exclude_from_kubernetes
urgency :low
def perform
@@ -32,4 +35,4 @@ module Namespaces
end
end
-Namespaces::InProductMarketingEmailsWorker.prepend_if_ee('EE::Namespaces::InProductMarketingEmailsWorker')
+Namespaces::InProductMarketingEmailsWorker.prepend_mod_with('Namespaces::InProductMarketingEmailsWorker')
diff --git a/app/workers/namespaces/onboarding_issue_created_worker.rb b/app/workers/namespaces/onboarding_issue_created_worker.rb
index e5e2c80e821..7b8b1a43078 100644
--- a/app/workers/namespaces/onboarding_issue_created_worker.rb
+++ b/app/workers/namespaces/onboarding_issue_created_worker.rb
@@ -4,7 +4,10 @@ module Namespaces
class OnboardingIssueCreatedWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :issue_tracking
+ tags :exclude_from_kubernetes
urgency :low
deduplicate :until_executing
diff --git a/app/workers/namespaces/onboarding_pipeline_created_worker.rb b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
index e1de6d0046b..128d7b6aa06 100644
--- a/app/workers/namespaces/onboarding_pipeline_created_worker.rb
+++ b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
@@ -4,7 +4,10 @@ module Namespaces
class OnboardingPipelineCreatedWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :subgroups
+ tags :exclude_from_kubernetes
urgency :low
deduplicate :until_executing
diff --git a/app/workers/namespaces/onboarding_progress_worker.rb b/app/workers/namespaces/onboarding_progress_worker.rb
index 9cb5a23cf31..d4db55a9207 100644
--- a/app/workers/namespaces/onboarding_progress_worker.rb
+++ b/app/workers/namespaces/onboarding_progress_worker.rb
@@ -4,7 +4,10 @@ module Namespaces
class OnboardingProgressWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :product_analytics
+ tags :exclude_from_kubernetes
urgency :low
deduplicate :until_executed
diff --git a/app/workers/namespaces/onboarding_user_added_worker.rb b/app/workers/namespaces/onboarding_user_added_worker.rb
index 02608268d6f..8c85dfafa12 100644
--- a/app/workers/namespaces/onboarding_user_added_worker.rb
+++ b/app/workers/namespaces/onboarding_user_added_worker.rb
@@ -4,7 +4,10 @@ module Namespaces
class OnboardingUserAddedWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :users
+ tags :exclude_from_kubernetes
urgency :low
idempotent!
diff --git a/app/workers/namespaces/prune_aggregation_schedules_worker.rb b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
index b94c8b7b4ba..0ea27c532ae 100644
--- a/app/workers/namespaces/prune_aggregation_schedules_worker.rb
+++ b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
@@ -3,6 +3,8 @@
module Namespaces
class PruneAggregationSchedulesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :source_code_management
diff --git a/app/workers/namespaces/root_statistics_worker.rb b/app/workers/namespaces/root_statistics_worker.rb
index 5d4b510ceb7..92bf2e22020 100644
--- a/app/workers/namespaces/root_statistics_worker.rb
+++ b/app/workers/namespaces/root_statistics_worker.rb
@@ -4,6 +4,8 @@ module Namespaces
class RootStatisticsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :update_namespace_statistics
feature_category :source_code_management
idempotent!
diff --git a/app/workers/namespaces/schedule_aggregation_worker.rb b/app/workers/namespaces/schedule_aggregation_worker.rb
index cbf5ed44572..cee273688e9 100644
--- a/app/workers/namespaces/schedule_aggregation_worker.rb
+++ b/app/workers/namespaces/schedule_aggregation_worker.rb
@@ -4,6 +4,8 @@ module Namespaces
class ScheduleAggregationWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :update_namespace_statistics
feature_category :source_code_management
idempotent!
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index c08f4b4cd75..a579b828354 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -2,6 +2,8 @@
class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include NewIssuable
feature_category :issue_tracking
@@ -18,7 +20,7 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
issuable.create_cross_references!(user)
Issues::AfterCreateService
- .new(issuable.project, user)
+ .new(project: issuable.project, current_user: user)
.execute(issuable)
end
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 2d28561488b..574c73ad3b5 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -2,6 +2,8 @@
class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include NewIssuable
feature_category :code_review
@@ -13,7 +15,7 @@ class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
return unless objects_found?(merge_request_id, user_id)
MergeRequests::AfterCreateService
- .new(issuable.target_project, user)
+ .new(project: issuable.target_project, current_user: user)
.execute(issuable)
end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 2bb2d0db55c..566bb9a9057 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -3,6 +3,8 @@
class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :issue_tracking
urgency :high
worker_resource_boundary :cpu
diff --git a/app/workers/object_pool/create_worker.rb b/app/workers/object_pool/create_worker.rb
index cf87ad95077..586b81fcd30 100644
--- a/app/workers/object_pool/create_worker.rb
+++ b/app/workers/object_pool/create_worker.rb
@@ -3,6 +3,8 @@
module ObjectPool
class CreateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ObjectPoolQueue
include ExclusiveLeaseGuard
@@ -28,7 +30,7 @@ module ObjectPool
pool.create_object_pool
pool.mark_ready
- rescue => e
+ rescue StandardError => e
pool.mark_failed
raise e
end
diff --git a/app/workers/object_pool/destroy_worker.rb b/app/workers/object_pool/destroy_worker.rb
index d42cee59d03..297780b20bd 100644
--- a/app/workers/object_pool/destroy_worker.rb
+++ b/app/workers/object_pool/destroy_worker.rb
@@ -3,6 +3,8 @@
module ObjectPool
class DestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ObjectPoolQueue
def perform(pool_repository_id)
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
index 8103c04b507..282a8f54695 100644
--- a/app/workers/object_pool/join_worker.rb
+++ b/app/workers/object_pool/join_worker.rb
@@ -3,6 +3,8 @@
module ObjectPool
class JoinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ObjectPoolQueue
worker_resource_boundary :cpu
diff --git a/app/workers/object_pool/schedule_join_worker.rb b/app/workers/object_pool/schedule_join_worker.rb
index c00bb2967f2..44208208d04 100644
--- a/app/workers/object_pool/schedule_join_worker.rb
+++ b/app/workers/object_pool/schedule_join_worker.rb
@@ -3,6 +3,8 @@
module ObjectPool
class ScheduleJoinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ObjectPoolQueue
def perform(pool_id)
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 666bacb0188..7323ab50370 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -4,6 +4,8 @@
module ObjectStorage
class MigrateUploadsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ObjectStorageQueue
feature_category_not_owned!
@@ -48,7 +50,7 @@ module ObjectStorage
Gitlab::AppLogger.info header(success, failures)
Gitlab::AppLogger.warn failures(failures)
- raise MigrationFailures.new(failures.map(&:error)) if failures.any?
+ raise MigrationFailures, failures.map(&:error) if failures.any?
end
def header(success, failures)
@@ -132,7 +134,7 @@ module ObjectStorage
def process_uploader(uploader)
MigrationResult.new(uploader.upload).tap do |result|
uploader.migrate!(@to_store)
- rescue => e
+ rescue StandardError => e
result.error = e
end
end
diff --git a/app/workers/packages/composer/cache_cleanup_worker.rb b/app/workers/packages/composer/cache_cleanup_worker.rb
index 638e50e18c4..1d47ef87962 100644
--- a/app/workers/packages/composer/cache_cleanup_worker.rb
+++ b/app/workers/packages/composer/cache_cleanup_worker.rb
@@ -4,9 +4,12 @@ module Packages
module Composer
class CacheCleanupWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :package_registry
+ tags :exclude_from_kubernetes
idempotent!
@@ -22,7 +25,7 @@ module Packages
rescue ActiveRecord::RecordNotFound
# ignore. likely due to object already being deleted.
end
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e)
end
end
diff --git a/app/workers/packages/composer/cache_update_worker.rb b/app/workers/packages/composer/cache_update_worker.rb
index 664fb23284f..d87abf2e256 100644
--- a/app/workers/packages/composer/cache_update_worker.rb
+++ b/app/workers/packages/composer/cache_update_worker.rb
@@ -5,7 +5,10 @@ module Packages
class CacheUpdateWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :package_registry
+ tags :exclude_from_kubernetes
idempotent!
@@ -15,7 +18,7 @@ module Packages
return unless project
Gitlab::Composer::Cache.new(project: project, name: package_name, last_page_sha: last_page_sha).execute
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
end
end
diff --git a/app/workers/packages/debian/process_changes_worker.rb b/app/workers/packages/debian/process_changes_worker.rb
new file mode 100644
index 00000000000..edc366a7597
--- /dev/null
+++ b/app/workers/packages/debian/process_changes_worker.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class ProcessChangesWorker
+ include ApplicationWorker
+ include Gitlab::Utils::StrongMemoize
+
+ deduplicate :until_executed
+ idempotent!
+
+ queue_namespace :package_repositories
+ feature_category :package_registry
+ tags :exclude_from_kubernetes
+
+ def perform(package_file_id, user_id)
+ @package_file_id = package_file_id
+ @user_id = user_id
+
+ return unless package_file && user
+
+ ::Packages::Debian::ProcessChangesService.new(package_file, user).execute
+ rescue ArgumentError,
+ Packages::Debian::ExtractChangesMetadataService::ExtractionError,
+ Packages::Debian::ExtractDebMetadataService::CommandFailedError,
+ Packages::Debian::ExtractMetadataService::ExtractionError,
+ Packages::Debian::ParseDebian822Service::InvalidDebian822Error,
+ ActiveRecord::RecordNotFound => e
+ Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id)
+ package_file.destroy!
+ end
+
+ private
+
+ attr_reader :package_file_id, :user_id
+
+ def package_file
+ strong_memoize(:package_file) do
+ ::Packages::PackageFile.find_by_id(package_file_id)
+ end
+ end
+
+ def user
+ strong_memoize(:user) do
+ ::User.find_by_id(user_id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/packages/go/sync_packages_worker.rb b/app/workers/packages/go/sync_packages_worker.rb
index e41f27f2252..c5f631c47db 100644
--- a/app/workers/packages/go/sync_packages_worker.rb
+++ b/app/workers/packages/go/sync_packages_worker.rb
@@ -4,10 +4,13 @@ module Packages
module Go
class SyncPackagesWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include Gitlab::Golang
queue_namespace :package_repositories
feature_category :package_registry
+ tags :exclude_from_kubernetes
deduplicate :until_executing
idempotent!
diff --git a/app/workers/packages/maven/metadata/sync_worker.rb b/app/workers/packages/maven/metadata/sync_worker.rb
index eb7abf4cdd0..c53117a08c5 100644
--- a/app/workers/packages/maven/metadata/sync_worker.rb
+++ b/app/workers/packages/maven/metadata/sync_worker.rb
@@ -5,10 +5,13 @@ module Packages
module Metadata
class SyncWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include Gitlab::Utils::StrongMemoize
queue_namespace :package_repositories
feature_category :package_registry
+ tags :exclude_from_kubernetes
deduplicate :until_executing
idempotent!
@@ -30,10 +33,10 @@ module Packages
if result.success?
log_extra_metadata_on_done(:message, result.message)
else
- raise SyncError.new(result.message)
+ raise SyncError, result.message
end
- raise SyncError.new(result.message) unless result.success?
+ raise SyncError, result.message unless result.success?
end
private
diff --git a/app/workers/packages/nuget/extraction_worker.rb b/app/workers/packages/nuget/extraction_worker.rb
index 820304a9f3b..4128b229ebe 100644
--- a/app/workers/packages/nuget/extraction_worker.rb
+++ b/app/workers/packages/nuget/extraction_worker.rb
@@ -5,6 +5,8 @@ module Packages
class ExtractionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :package_repositories
feature_category :package_registry
@@ -15,10 +17,9 @@ module Packages
::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file).execute
- rescue ::Packages::Nuget::MetadataExtractionService::ExtractionError,
- ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id)
- package_file.package.destroy!
+ package_file.package.update_column(:status, :error)
end
end
end
diff --git a/app/workers/packages/rubygems/extraction_worker.rb b/app/workers/packages/rubygems/extraction_worker.rb
index 1e5cd0b54ce..fc32654a2c1 100644
--- a/app/workers/packages/rubygems/extraction_worker.rb
+++ b/app/workers/packages/rubygems/extraction_worker.rb
@@ -5,12 +5,13 @@ module Packages
class ExtractionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
queue_namespace :package_repositories
feature_category :package_registry
+ tags :exclude_from_kubernetes
deduplicate :until_executing
- idempotent!
-
def perform(package_file_id)
package_file = ::Packages::PackageFile.find_by_id(package_file_id)
@@ -18,9 +19,9 @@ module Packages
::Packages::Rubygems::ProcessGemService.new(package_file).execute
- rescue ::Packages::Rubygems::ProcessGemService::ExtractionError => e
+ rescue StandardError => e
Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id)
- package_file.package.destroy!
+ package_file.package.update_column(:status, :error)
end
end
end
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
index cb24441d2f7..cc720676214 100644
--- a/app/workers/pages_domain_removal_cron_worker.rb
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -2,6 +2,8 @@
class PagesDomainRemovalCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :pages
@@ -10,7 +12,7 @@ class PagesDomainRemovalCronWorker # rubocop:disable Scalability/IdempotentWorke
def perform
PagesDomain.for_removal.with_logging_info.find_each do |domain|
with_context(project: domain.project) { domain.destroy! }
- rescue => e
+ rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e)
end
end
diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
index fe6d516d3cf..c99eed8a8df 100644
--- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
@@ -2,6 +2,8 @@
class PagesDomainSslRenewalCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :pages
diff --git a/app/workers/pages_domain_ssl_renewal_worker.rb b/app/workers/pages_domain_ssl_renewal_worker.rb
index 125ba343948..2ab41aab795 100644
--- a/app/workers/pages_domain_ssl_renewal_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_worker.rb
@@ -3,8 +3,10 @@
class PagesDomainSslRenewalWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :pages
- tags :requires_disk_io
+ tags :requires_disk_io, :exclude_from_kubernetes
def perform(domain_id)
domain = PagesDomain.find_by_id(domain_id)
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index a30f0b981d8..ec63004716a 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -2,6 +2,8 @@
class PagesDomainVerificationCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :pages
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index ff0463481cd..b67b1b4d8ee 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -3,8 +3,10 @@
class PagesDomainVerificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :pages
- tags :requires_disk_io
+ tags :requires_disk_io, :exclude_from_kubernetes
# rubocop: disable CodeReuse/ActiveRecord
def perform(domain_id)
diff --git a/app/workers/pages_remove_worker.rb b/app/workers/pages_remove_worker.rb
index 67ea18545a7..3e60df9027a 100644
--- a/app/workers/pages_remove_worker.rb
+++ b/app/workers/pages_remove_worker.rb
@@ -6,6 +6,7 @@ class PagesRemoveWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
feature_category :pages
+ tags :exclude_from_kubernetes
loggable_arguments 0
def perform(project_id)
diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb
index 5d395c9e38a..0d80ec28310 100644
--- a/app/workers/pages_transfer_worker.rb
+++ b/app/workers/pages_transfer_worker.rb
@@ -3,9 +3,12 @@
class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
TransferFailedError = Class.new(StandardError)
feature_category :pages
+ tags :exclude_from_kubernetes
loggable_arguments 0, 1
def perform(method, args)
diff --git a/app/workers/pages_update_configuration_worker.rb b/app/workers/pages_update_configuration_worker.rb
index ca5016dc782..8bb9f76670b 100644
--- a/app/workers/pages_update_configuration_worker.rb
+++ b/app/workers/pages_update_configuration_worker.rb
@@ -3,8 +3,11 @@
class PagesUpdateConfigurationWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
feature_category :pages
+ tags :exclude_from_kubernetes
def self.perform_async(*args)
return unless ::Settings.pages.local_store.enabled
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 0c561626f8c..ee394271653 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -6,7 +6,7 @@ class PagesWorker # rubocop:disable Scalability/IdempotentWorker
sidekiq_options retry: 3
feature_category :pages
loggable_arguments 0, 1
- tags :requires_disk_io
+ tags :requires_disk_io, :exclude_from_kubernetes
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/partition_creation_worker.rb b/app/workers/partition_creation_worker.rb
index 119ecd28003..2b21741d6c2 100644
--- a/app/workers/partition_creation_worker.rb
+++ b/app/workers/partition_creation_worker.rb
@@ -2,6 +2,8 @@
class PartitionCreationWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :database
diff --git a/app/workers/personal_access_tokens/expired_notification_worker.rb b/app/workers/personal_access_tokens/expired_notification_worker.rb
index 2ff64ec51f3..73568960d38 100644
--- a/app/workers/personal_access_tokens/expired_notification_worker.rb
+++ b/app/workers/personal_access_tokens/expired_notification_worker.rb
@@ -3,9 +3,12 @@
module PersonalAccessTokens
class ExpiredNotificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :authentication_and_authorization
+ tags :exclude_from_kubernetes
def perform(*args)
notification_service = NotificationService.new
diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb
index 7a016c85a64..aaca78e3c63 100644
--- a/app/workers/personal_access_tokens/expiring_worker.rb
+++ b/app/workers/personal_access_tokens/expiring_worker.rb
@@ -3,6 +3,8 @@
module PersonalAccessTokens
class ExpiringWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :authentication_and_authorization
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index b8dd4768cfb..fbb672f52e3 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -2,6 +2,8 @@
class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_hooks
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 1eb9b4ce089..fdab10d7dda 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -2,6 +2,8 @@
class PipelineMetricsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
urgency :high
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index f4b43106bf2..619570dcf41 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -2,6 +2,8 @@
class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
urgency :high
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index f0929b92bd0..dc14789fe73 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -2,12 +2,15 @@
class PipelineProcessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
feature_category :continuous_integration
urgency :high
loggable_arguments 1
+ data_consistency :delayed, feature_flag: :load_balancing_for_pipeline_process_worker
# rubocop: disable CodeReuse/ActiveRecord
# `_build_ids` is deprecated and will be removed in 14.0
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index d81b978f9b0..f1248ec9e58 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -2,6 +2,8 @@
class PipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :continuous_integration
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 7db4ab8fe0b..e8feb4f2db2 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -4,6 +4,8 @@
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/232806
class PipelineUpdateWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 313b901c08e..ce985492935 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -2,6 +2,8 @@
class PostReceive # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include Gitlab::Experiment::Dsl
feature_category :source_code_management
@@ -123,7 +125,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
def after_project_changes_hooks(project, user, refs, changes)
experiment(:new_project_readme, actor: user).track_initial_writes(project)
- experiment(:empty_repo_upload, project: project).track(:initial_write) if project.empty_repo?
+ experiment(:empty_repo_upload, project: project).track_initial_write
repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs)
SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks)
Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes)
@@ -134,4 +136,4 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end
end
-PostReceive.prepend_if_ee('EE::PostReceive')
+PostReceive.prepend_mod_with('PostReceive')
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 3c7af641f16..54ffe8d3b10 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -10,6 +10,8 @@
class ProcessCommitWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
urgency :high
weight 3
@@ -51,7 +53,7 @@ class ProcessCommitWorker
# therefore we use IssueCollection here and skip the authorization check in
# Issues::CloseService#execute.
IssueCollection.new(issues).updatable_by_user(user).each do |issue|
- Issues::CloseService.new(project, author)
+ Issues::CloseService.new(project: project, current_user: author)
.close_issue(issue, closed_via: commit)
end
end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 8a9c166e5df..d2796cdb697 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -4,6 +4,8 @@
class ProjectCacheWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
LEASE_TIMEOUT = 15.minutes.to_i
feature_category :source_code_management
@@ -61,4 +63,4 @@ class ProjectCacheWorker
end
end
-ProjectCacheWorker.prepend_if_ee('EE::ProjectCacheWorker')
+ProjectCacheWorker.prepend_mod_with('ProjectCacheWorker')
diff --git a/app/workers/project_daily_statistics_worker.rb b/app/workers/project_daily_statistics_worker.rb
index 2166655115d..7d673ec00d9 100644
--- a/app/workers/project_daily_statistics_worker.rb
+++ b/app/workers/project_daily_statistics_worker.rb
@@ -4,6 +4,8 @@
class ProjectDailyStatisticsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
def perform(project_id)
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index 99d51fc5c2e..be11fa65028 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -2,10 +2,12 @@
class ProjectDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExceptionBacktrace
feature_category :source_code_management
- tags :requires_disk_io
+ tags :requires_disk_io, :exclude_from_kubernetes
def perform(project_id, user_id, params)
project = Project.find(project_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 84c3a3e52d0..967be3b3e81 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -3,16 +3,18 @@
class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
sidekiq_options dead: false
feature_category :integrations
worker_has_external_dependencies!
def perform(hook_id, data)
data = data.with_indifferent_access
- service = Service.find(hook_id)
- service.execute(data)
- rescue => error
- service_class = service&.class&.name || "Not Found"
- logger.error class: self.class.name, service_class: service_class, message: error.message
+ integration = Integration.find(hook_id)
+ integration.execute(data)
+ rescue StandardError => error
+ integration_class = integration&.class&.name || "Not Found"
+ logger.error class: self.class.name, service_class: integration_class, message: error.message
end
end
diff --git a/app/workers/projects/git_garbage_collect_worker.rb b/app/workers/projects/git_garbage_collect_worker.rb
index 4f908529b34..0d67a8ac30e 100644
--- a/app/workers/projects/git_garbage_collect_worker.rb
+++ b/app/workers/projects/git_garbage_collect_worker.rb
@@ -5,6 +5,8 @@ module Projects
extend ::Gitlab::Utils::Override
include GitGarbageCollectMethods
+ tags :exclude_from_kubernetes
+
private
override :find_resource
@@ -24,7 +26,7 @@ module Projects
return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
::Gitlab::Cleanup::OrphanLfsFileReferences.new(resource, dry_run: false, logger: logger).run!
- rescue => err
+ rescue StandardError => err
Gitlab::GitLogger.warn(message: "Cleaning up orphan LFS objects files failed", error: err.message)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
end
diff --git a/app/workers/projects/post_creation_worker.rb b/app/workers/projects/post_creation_worker.rb
index 2ca62e582b6..1970f79729f 100644
--- a/app/workers/projects/post_creation_worker.rb
+++ b/app/workers/projects/post_creation_worker.rb
@@ -4,7 +4,10 @@ module Projects
class PostCreationWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
+ tags :exclude_from_kubernetes
idempotent!
def perform(project_id)
diff --git a/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb b/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb
index 3841ae9b922..55530bff7c1 100644
--- a/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb
+++ b/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb
@@ -4,6 +4,8 @@ module Projects
class ScheduleBulkRepositoryShardMovesWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
feature_category :gitaly
urgency :throttled
diff --git a/app/workers/projects/update_repository_storage_worker.rb b/app/workers/projects/update_repository_storage_worker.rb
index f4c44458446..1de1c95e043 100644
--- a/app/workers/projects/update_repository_storage_worker.rb
+++ b/app/workers/projects/update_repository_storage_worker.rb
@@ -5,6 +5,8 @@ module Projects
extend ::Gitlab::Utils::Override
include ::UpdateRepositoryStorageWorker
+ sidekiq_options retry: 3
+
private
override :find_repository_storage_move
diff --git a/app/workers/prometheus/create_default_alerts_worker.rb b/app/workers/prometheus/create_default_alerts_worker.rb
index 2c4fefa9ece..0dba752ced1 100644
--- a/app/workers/prometheus/create_default_alerts_worker.rb
+++ b/app/workers/prometheus/create_default_alerts_worker.rb
@@ -4,6 +4,8 @@ module Prometheus
class CreateDefaultAlertsWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :incident_management
urgency :high
idempotent!
diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb
index 01155753877..6881740461f 100644
--- a/app/workers/propagate_integration_group_worker.rb
+++ b/app/workers/propagate_integration_group_worker.rb
@@ -3,15 +3,18 @@
class PropagateIntegrationGroupWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
+ tags :exclude_from_kubernetes
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(integration_id, min_id, max_id)
- integration = Service.find_by_id(integration_id)
+ integration = Integration.find_by_id(integration_id)
return unless integration
- batch = if integration.instance?
+ batch = if integration.instance_level?
Group.where(id: min_id..max_id).without_integration(integration)
else
integration.group.descendants.where(id: min_id..max_id).without_integration(integration)
diff --git a/app/workers/propagate_integration_inherit_descendant_worker.rb b/app/workers/propagate_integration_inherit_descendant_worker.rb
index d589619818c..9067af12de3 100644
--- a/app/workers/propagate_integration_inherit_descendant_worker.rb
+++ b/app/workers/propagate_integration_inherit_descendant_worker.rb
@@ -3,15 +3,18 @@
class PropagateIntegrationInheritDescendantWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
+ tags :exclude_from_kubernetes
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(integration_id, min_id, max_id)
- integration = Service.find_by_id(integration_id)
+ integration = Integration.find_by_id(integration_id)
return unless integration
- batch = Service.inherited_descendants_from_self_or_ancestors_from(integration).where(id: min_id..max_id)
+ batch = Integration.inherited_descendants_from_self_or_ancestors_from(integration).where(id: min_id..max_id)
BulkUpdateIntegrationService.new(integration, batch).execute
end
diff --git a/app/workers/propagate_integration_inherit_worker.rb b/app/workers/propagate_integration_inherit_worker.rb
index 40d67c6d3bf..e7649d6714f 100644
--- a/app/workers/propagate_integration_inherit_worker.rb
+++ b/app/workers/propagate_integration_inherit_worker.rb
@@ -3,15 +3,18 @@
class PropagateIntegrationInheritWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
+ tags :exclude_from_kubernetes
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(integration_id, min_id, max_id)
- integration = Service.find_by_id(integration_id)
+ integration = Integration.find_by_id(integration_id)
return unless integration
- batch = Service.where(id: min_id..max_id).by_type(integration.type).inherit_from_id(integration.id)
+ batch = Integration.where(id: min_id..max_id).by_type(integration.type).inherit_from_id(integration.id)
BulkUpdateIntegrationService.new(integration, batch).execute
end
diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb
index 188d81e5fc1..90cf27c4176 100644
--- a/app/workers/propagate_integration_project_worker.rb
+++ b/app/workers/propagate_integration_project_worker.rb
@@ -3,16 +3,19 @@
class PropagateIntegrationProjectWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
+ tags :exclude_from_kubernetes
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(integration_id, min_id, max_id)
- integration = Service.find_by_id(integration_id)
+ integration = Integration.find_by_id(integration_id)
return unless integration
batch = Project.where(id: min_id..max_id).without_integration(integration)
- batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_id
+ batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_level?
return if batch.empty?
diff --git a/app/workers/propagate_integration_worker.rb b/app/workers/propagate_integration_worker.rb
index bb954b12a25..5e694529bc0 100644
--- a/app/workers/propagate_integration_worker.rb
+++ b/app/workers/propagate_integration_worker.rb
@@ -3,6 +3,8 @@
class PropagateIntegrationWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
idempotent!
loggable_arguments 1
@@ -10,6 +12,6 @@ class PropagateIntegrationWorker
# TODO: Keep overwrite parameter for backwards compatibility. Remove after >= 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/255382
def perform(integration_id, overwrite = nil)
- Admin::PropagateIntegrationService.propagate(Service.find(integration_id))
+ Admin::PropagateIntegrationService.propagate(Integration.find(integration_id))
end
end
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index b02525b5106..149577b15cd 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -4,6 +4,8 @@
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
LEASE_TIMEOUT = 4.hours.to_i
@@ -12,7 +14,7 @@ class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWor
def perform(template_id)
return unless try_obtain_lease_for(template_id)
- Admin::PropagateServiceTemplate.propagate(Service.find_by(id: template_id))
+ Admin::PropagateServiceTemplate.propagate(Integration.find_by(id: template_id))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 330de4c7cba..59d324bc573 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -2,6 +2,8 @@
class PruneOldEventsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
index a8e81a24ecd..abfaabbf01d 100644
--- a/app/workers/prune_web_hook_logs_worker.rb
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -4,6 +4,8 @@
# table.
class PruneWebHookLogsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/purge_dependency_proxy_cache_worker.rb b/app/workers/purge_dependency_proxy_cache_worker.rb
index b4c88592543..9f1ea8a6eb4 100644
--- a/app/workers/purge_dependency_proxy_cache_worker.rb
+++ b/app/workers/purge_dependency_proxy_cache_worker.rb
@@ -2,6 +2,8 @@
class PurgeDependencyProxyCacheWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include Gitlab::Allowable
idempotent!
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index ee9ae827bb6..664905eb9e5 100644
--- a/app/workers/rebase_worker.rb
+++ b/app/workers/rebase_worker.rb
@@ -5,6 +5,8 @@
class RebaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
weight 2
loggable_arguments 2
@@ -14,7 +16,7 @@ class RebaseWorker # rubocop:disable Scalability/IdempotentWorker
merge_request = MergeRequest.find(merge_request_id)
MergeRequests::RebaseService
- .new(merge_request.source_project, current_user)
+ .new(project: merge_request.source_project, current_user: current_user)
.execute(merge_request, skip_ci: skip_ci)
end
end
diff --git a/app/workers/releases/create_evidence_worker.rb b/app/workers/releases/create_evidence_worker.rb
index d22329216f9..bd790e8d0ee 100644
--- a/app/workers/releases/create_evidence_worker.rb
+++ b/app/workers/releases/create_evidence_worker.rb
@@ -4,7 +4,10 @@ module Releases
class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :release_evidence
+ tags :exclude_from_kubernetes
# pipeline_id is optional for backward compatibility with existing jobs
# caller should always try to provide the pipeline and pass nil only
diff --git a/app/workers/releases/manage_evidence_worker.rb b/app/workers/releases/manage_evidence_worker.rb
index 8a925d22cea..88b6c4aea06 100644
--- a/app/workers/releases/manage_evidence_worker.rb
+++ b/app/workers/releases/manage_evidence_worker.rb
@@ -3,9 +3,12 @@
module Releases
class ManageEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :release_evidence
+ tags :exclude_from_kubernetes
def perform
releases = Release.without_evidence.released_within_2hrs
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
index 33f5002014d..39a7c0fc79d 100644
--- a/app/workers/remote_mirror_notification_worker.rb
+++ b/app/workers/remote_mirror_notification_worker.rb
@@ -3,6 +3,8 @@
class RemoteMirrorNotificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
weight 2
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 3f1a484f384..edf3a02cff5 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -2,6 +2,8 @@
class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :authentication_and_authorization
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index fc2ec047e1c..9940953207e 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -2,6 +2,8 @@
class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :authentication_and_authorization
@@ -26,7 +28,7 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
Users::DestroyService.new(nil).execute(expired_user, skip_authorization: true)
end
end
- rescue => ex
+ rescue StandardError => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
end
diff --git a/app/workers/remove_unaccepted_member_invites_worker.rb b/app/workers/remove_unaccepted_member_invites_worker.rb
index 4b75b43791e..c1f8e3881f1 100644
--- a/app/workers/remove_unaccepted_member_invites_worker.rb
+++ b/app/workers/remove_unaccepted_member_invites_worker.rb
@@ -2,9 +2,12 @@
class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :authentication_and_authorization
+ tags :exclude_from_kubernetes
urgency :low
idempotent!
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index 76ab23ebbd5..b42883549ca 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -2,6 +2,8 @@
class RemoveUnreferencedLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index 84f61a60953..84cafba17cf 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -2,6 +2,8 @@
class RepositoryArchiveCacheWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index d47f738ccb0..a8744638d7b 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -3,6 +3,8 @@
module RepositoryCheck
class BatchWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include RepositoryCheckQueue
include ExclusiveLeaseGuard
@@ -95,4 +97,4 @@ module RepositoryCheck
end
end
-RepositoryCheck::BatchWorker.prepend_if_ee('::EE::RepositoryCheck::BatchWorker')
+RepositoryCheck::BatchWorker.prepend_mod_with('RepositoryCheck::BatchWorker')
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index 1689b9bf251..bc19b42da1a 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -3,6 +3,8 @@
module RepositoryCheck
class ClearWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include RepositoryCheckQueue
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
index d7a145011fa..30734926765 100644
--- a/app/workers/repository_check/dispatch_worker.rb
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -3,6 +3,8 @@
module RepositoryCheck
class DispatchWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index d757b87c23a..a9a8201205e 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -3,6 +3,8 @@
module RepositoryCheck
class SingleRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include RepositoryCheckQueue
def perform(project_id)
@@ -67,4 +69,4 @@ module RepositoryCheck
end
end
-RepositoryCheck::SingleRepositoryWorker.prepend_if_ee('::EE::RepositoryCheck::SingleRepositoryWorker')
+RepositoryCheck::SingleRepositoryWorker.prepend_mod_with('RepositoryCheck::SingleRepositoryWorker')
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index fc7999e7837..06a6f5b0600 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -2,6 +2,8 @@
class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ProjectStartImport
include ProjectImportOptions
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 90764d7374d..0f86d55df22 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -63,4 +63,4 @@ class RepositoryImportWorker # rubocop:disable Scalability/IdempotentWorker
end
end
-RepositoryImportWorker.prepend_if_ee('EE::RepositoryImportWorker')
+RepositoryImportWorker.prepend_mod_with('RepositoryImportWorker')
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
index 5e632b1b1ca..48158cda857 100644
--- a/app/workers/repository_remove_remote_worker.rb
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -2,6 +2,8 @@
class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExclusiveLeaseGuard
feature_category :source_code_management
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 106f04d9409..35c18177a81 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -2,6 +2,8 @@
class RequestsProfilesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 605dd624260..553153848c7 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -2,6 +2,8 @@
class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_creation
@@ -25,7 +27,7 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
.execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
rescue Ci::CreatePipelineService::CreateError
# no-op. This is a user operation error such as corrupted .gitlab-ci.yml.
- rescue => e
+ rescue StandardError => e
error(schedule, e)
end
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index 967032f99e5..b5ea5298879 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -2,9 +2,12 @@
class ScheduleMergeRequestCleanupRefsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :code_review
+ tags :exclude_from_kubernetes
idempotent!
# Based on existing data, MergeRequestCleanupRefsWorker can run 3 jobs per
diff --git a/app/workers/schedule_migrate_external_diffs_worker.rb b/app/workers/schedule_migrate_external_diffs_worker.rb
index 70e4d56562b..ecafe8f5e7d 100644
--- a/app/workers/schedule_migrate_external_diffs_worker.rb
+++ b/app/workers/schedule_migrate_external_diffs_worker.rb
@@ -2,6 +2,8 @@
class ScheduleMigrateExternalDiffsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext:
# This schedules the `MigrateExternalDiffsWorker`
# issue for adding context: https://gitlab.com/gitlab-org/gitlab/issues/202100
diff --git a/app/workers/self_monitoring_project_create_worker.rb b/app/workers/self_monitoring_project_create_worker.rb
index 8177efb1683..9dc3bb855fb 100644
--- a/app/workers/self_monitoring_project_create_worker.rb
+++ b/app/workers/self_monitoring_project_create_worker.rb
@@ -2,6 +2,8 @@
class SelfMonitoringProjectCreateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExclusiveLeaseGuard
include SelfMonitoringProjectWorker
diff --git a/app/workers/self_monitoring_project_delete_worker.rb b/app/workers/self_monitoring_project_delete_worker.rb
index 4fa05d71de5..c155c57dec7 100644
--- a/app/workers/self_monitoring_project_delete_worker.rb
+++ b/app/workers/self_monitoring_project_delete_worker.rb
@@ -2,6 +2,8 @@
class SelfMonitoringProjectDeleteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ExclusiveLeaseGuard
include SelfMonitoringProjectWorker
diff --git a/app/workers/service_desk_email_receiver_worker.rb b/app/workers/service_desk_email_receiver_worker.rb
index 8649034445c..cfe63e059bb 100644
--- a/app/workers/service_desk_email_receiver_worker.rb
+++ b/app/workers/service_desk_email_receiver_worker.rb
@@ -3,13 +3,14 @@
class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- def perform(raw)
- return unless ::Gitlab::ServiceDeskEmail.enabled?
-
- begin
- Gitlab::Email::ServiceDeskReceiver.new(raw).execute
- rescue => e
- handle_failure(raw, e)
- end
+ feature_category :service_desk
+ sidekiq_options retry: 3
+
+ def should_perform?
+ ::Gitlab::ServiceDeskEmail.enabled?
+ end
+
+ def receiver
+ @receiver ||= Gitlab::Email::ServiceDeskReceiver.new(raw)
end
end
diff --git a/app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb b/app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb
index ec3d9dbdf97..88b060a454a 100644
--- a/app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb
+++ b/app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb
@@ -4,6 +4,8 @@ module Snippets
class ScheduleBulkRepositoryShardMovesWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
idempotent!
feature_category :gitaly
urgency :throttled
diff --git a/app/workers/snippets/update_repository_storage_worker.rb b/app/workers/snippets/update_repository_storage_worker.rb
index 83b655e9986..ffb01e2623b 100644
--- a/app/workers/snippets/update_repository_storage_worker.rb
+++ b/app/workers/snippets/update_repository_storage_worker.rb
@@ -5,6 +5,8 @@ module Snippets
extend ::Gitlab::Utils::Override
include ::UpdateRepositoryStorageWorker
+ sidekiq_options retry: 3
+
private
override :find_repository_storage_move
diff --git a/app/workers/ssh_keys/expired_notification_worker.rb b/app/workers/ssh_keys/expired_notification_worker.rb
index ab6d1998773..9d5143fe655 100644
--- a/app/workers/ssh_keys/expired_notification_worker.rb
+++ b/app/workers/ssh_keys/expired_notification_worker.rb
@@ -3,15 +3,19 @@
module SshKeys
class ExpiredNotificationWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :compliance_management
+ tags :exclude_from_kubernetes
idempotent!
def perform
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
- User.with_ssh_key_expired_today.find_each do |user|
+ # rubocop:disable CodeReuse/ActiveRecord
+ User.with_ssh_key_expired_today.find_each(batch_size: 10_000) do |user|
with_context(user: user) do
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expired ssh key(s)"
@@ -19,6 +23,7 @@ module SshKeys
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: false }).execute
end
+ # rubocop:enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/ssh_keys/expiring_soon_notification_worker.rb b/app/workers/ssh_keys/expiring_soon_notification_worker.rb
index 3214cd7a242..1ec655b5cf5 100644
--- a/app/workers/ssh_keys/expiring_soon_notification_worker.rb
+++ b/app/workers/ssh_keys/expiring_soon_notification_worker.rb
@@ -3,15 +3,19 @@
module SshKeys
class ExpiringSoonNotificationWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :compliance_management
+ tags :exclude_from_kubernetes
idempotent!
def perform
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
- User.with_ssh_key_expiring_soon.find_each do |user|
+ # rubocop:disable CodeReuse/ActiveRecord
+ User.with_ssh_key_expiring_soon.find_each(batch_size: 10_000) do |user|
with_context(user: user) do
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring soon ssh key(s)"
@@ -20,6 +24,7 @@ module SshKeys
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: true }).execute
end
end
+ # rubocop:enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index 20db19536c3..e206a51a417 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -2,6 +2,8 @@
class StageUpdateWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index bd721df73c6..6b9f90ce1fc 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -2,6 +2,8 @@
class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :continuous_integration
@@ -73,7 +75,7 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::OptimisticLocking.retry_lock(build, 3, name: 'stuck_ci_jobs_worker_drop_build') do |b|
b.drop(reason)
end
- rescue => ex
+ rescue StandardError => ex
build.doom!
track_exception_for_build(ex, build)
diff --git a/app/workers/stuck_export_jobs_worker.rb b/app/workers/stuck_export_jobs_worker.rb
index 6d8d60d2fc0..398f2c915a9 100644
--- a/app/workers/stuck_export_jobs_worker.rb
+++ b/app/workers/stuck_export_jobs_worker.rb
@@ -3,6 +3,8 @@
# rubocop:disable Scalability/IdempotentWorker
class StuckExportJobsWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker updates export states inline and does not schedule
# other jobs.
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
index bea9d67b3e8..e50b218e1f6 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -2,6 +2,8 @@
class StuckMergeJobsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :code_review
diff --git a/app/workers/system_hook_push_worker.rb b/app/workers/system_hook_push_worker.rb
index ff1f2baf058..8c801f2bed8 100644
--- a/app/workers/system_hook_push_worker.rb
+++ b/app/workers/system_hook_push_worker.rb
@@ -3,6 +3,8 @@
class SystemHookPushWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
def perform(push_data, hook_id)
diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb
index b29d4168162..8a43ea3c2e0 100644
--- a/app/workers/todos_destroyer/confidential_issue_worker.rb
+++ b/app/workers/todos_destroyer/confidential_issue_worker.rb
@@ -3,6 +3,8 @@
module TodosDestroyer
class ConfidentialIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
def perform(issue_id = nil, project_id = nil)
diff --git a/app/workers/todos_destroyer/destroyed_issuable_worker.rb b/app/workers/todos_destroyer/destroyed_issuable_worker.rb
index 6ca1959ff34..a3a8147095e 100644
--- a/app/workers/todos_destroyer/destroyed_issuable_worker.rb
+++ b/app/workers/todos_destroyer/destroyed_issuable_worker.rb
@@ -3,8 +3,12 @@
module TodosDestroyer
class DestroyedIssuableWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
+ tags :exclude_from_kubernetes
+
idempotent!
def perform(target_id, target_type)
diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb
index 4996456dc91..166d8701f7a 100644
--- a/app/workers/todos_destroyer/entity_leave_worker.rb
+++ b/app/workers/todos_destroyer/entity_leave_worker.rb
@@ -3,6 +3,8 @@
module TodosDestroyer
class EntityLeaveWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
loggable_arguments 2
diff --git a/app/workers/todos_destroyer/group_private_worker.rb b/app/workers/todos_destroyer/group_private_worker.rb
index a1943bee2ec..30d1f74fb28 100644
--- a/app/workers/todos_destroyer/group_private_worker.rb
+++ b/app/workers/todos_destroyer/group_private_worker.rb
@@ -3,6 +3,8 @@
module TodosDestroyer
class GroupPrivateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
def perform(group_id)
diff --git a/app/workers/todos_destroyer/private_features_worker.rb b/app/workers/todos_destroyer/private_features_worker.rb
index 6e55467234a..d6a4260a464 100644
--- a/app/workers/todos_destroyer/private_features_worker.rb
+++ b/app/workers/todos_destroyer/private_features_worker.rb
@@ -3,6 +3,8 @@
module TodosDestroyer
class PrivateFeaturesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
def perform(project_id, user_id = nil)
diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb
index 2a06edc666e..c4fed03f11a 100644
--- a/app/workers/todos_destroyer/project_private_worker.rb
+++ b/app/workers/todos_destroyer/project_private_worker.rb
@@ -3,6 +3,8 @@
module TodosDestroyer
class ProjectPrivateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include TodosDestroyerQueue
def perform(project_id)
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 5876cfb1fe7..8322110b753 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -2,6 +2,8 @@
class TrendingProjectsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :source_code_management
diff --git a/app/workers/update_container_registry_info_worker.rb b/app/workers/update_container_registry_info_worker.rb
index 14a816f25ef..cf08c650d0d 100644
--- a/app/workers/update_container_registry_info_worker.rb
+++ b/app/workers/update_container_registry_info_worker.rb
@@ -2,6 +2,8 @@
class UpdateContainerRegistryInfoWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :container_registry
diff --git a/app/workers/update_external_pull_requests_worker.rb b/app/workers/update_external_pull_requests_worker.rb
index e916331ae82..ee47cbd6523 100644
--- a/app/workers/update_external_pull_requests_worker.rb
+++ b/app/workers/update_external_pull_requests_worker.rb
@@ -3,6 +3,8 @@
class UpdateExternalPullRequestsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
weight 3
loggable_arguments 2
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 63d11d33283..f1dd250f432 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -2,6 +2,8 @@
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include PipelineQueue
queue_namespace :pipeline_processing
diff --git a/app/workers/update_highest_role_worker.rb b/app/workers/update_highest_role_worker.rb
index 952f1e511ea..cecf3f99b50 100644
--- a/app/workers/update_highest_role_worker.rb
+++ b/app/workers/update_highest_role_worker.rb
@@ -3,6 +3,8 @@
class UpdateHighestRoleWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :utilization
urgency :high
weight 2
@@ -15,7 +17,7 @@ class UpdateHighestRoleWorker
return unless user.present?
- if user.active? && user.user_type.nil? && !user.internal?
+ if user.active? && user.human? && !user.internal?
Users::UpdateHighestMemberRoleService.new(user).execute
else
UserHighestRole.where(user_id: user_id).delete_all
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 46cb32e7f08..6f86a7e7e2f 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -3,6 +3,8 @@
class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :code_review
urgency :high
worker_resource_boundary :cpu
@@ -17,7 +19,7 @@ class UpdateMergeRequestsWorker # rubocop:disable Scalability/IdempotentWorker
user = User.find_by(id: user_id)
return unless user
- MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
+ MergeRequests::RefreshService.new(project: project, current_user: user).execute(oldrev, newrev, ref)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
index 336877d9f57..c93c32f4e75 100644
--- a/app/workers/update_project_statistics_worker.rb
+++ b/app/workers/update_project_statistics_worker.rb
@@ -4,6 +4,8 @@
class UpdateProjectStatisticsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
# project_id - The ID of the project for which to flush the cache.
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index ce43b56bbd8..765e3a63e75 100644
--- a/app/workers/upload_checksum_worker.rb
+++ b/app/workers/upload_checksum_worker.rb
@@ -3,6 +3,8 @@
class UploadChecksumWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :geo_replication
def perform(upload_id)
diff --git a/app/workers/user_status_cleanup/batch_worker.rb b/app/workers/user_status_cleanup/batch_worker.rb
index 0c1087cc4d2..f46b4119f9b 100644
--- a/app/workers/user_status_cleanup/batch_worker.rb
+++ b/app/workers/user_status_cleanup/batch_worker.rb
@@ -4,11 +4,14 @@ module UserStatusCleanup
# This worker will run every minute to look for user status records to clean up.
class BatchWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :users
+ tags :exclude_from_kubernetes
idempotent!
diff --git a/app/workers/users/create_statistics_worker.rb b/app/workers/users/create_statistics_worker.rb
index fb1b192577f..e44039f2016 100644
--- a/app/workers/users/create_statistics_worker.rb
+++ b/app/workers/users/create_statistics_worker.rb
@@ -3,6 +3,8 @@
module Users
class CreateStatisticsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb
new file mode 100644
index 00000000000..e583823312f
--- /dev/null
+++ b/app/workers/users/deactivate_dormant_users_worker.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Users
+ class DeactivateDormantUsersWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ include CronjobQueue
+
+ feature_category :utilization
+ tags :exclude_from_kubernetes
+
+ NUMBER_OF_BATCHES = 50
+ BATCH_SIZE = 200
+ PAUSE_SECONDS = 0.25
+
+ def perform
+ return if Gitlab.com?
+
+ return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users
+
+ with_context(caller_id: self.class.name.to_s) do
+ NUMBER_OF_BATCHES.times do
+ result = User.connection.execute(update_query)
+
+ break if result.cmd_tuples == 0
+
+ sleep(PAUSE_SECONDS)
+ end
+ end
+ end
+
+ private
+
+ def update_query
+ <<~SQL
+ UPDATE "users"
+ SET "state" = 'deactivated'
+ WHERE "users"."id" IN (
+ (#{users.dormant.to_sql})
+ UNION
+ (#{users.with_no_activity.to_sql})
+ LIMIT #{BATCH_SIZE}
+ )
+ SQL
+ end
+
+ def users
+ User.select(:id).limit(BATCH_SIZE)
+ end
+ end
+end
diff --git a/app/workers/users/update_open_issue_count_worker.rb b/app/workers/users/update_open_issue_count_worker.rb
new file mode 100644
index 00000000000..d9e313d53df
--- /dev/null
+++ b/app/workers/users/update_open_issue_count_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Users
+ class UpdateOpenIssueCountWorker
+ include ApplicationWorker
+
+ feature_category :users
+ tags :exclude_from_kubernetes
+ idempotent!
+
+ def perform(target_user_ids)
+ target_user_ids = Array.wrap(target_user_ids)
+
+ raise ArgumentError, 'No target user ID provided' if target_user_ids.empty?
+
+ target_users = User.id_in(target_user_ids)
+ raise ArgumentError, 'No valid target user ID provided' if target_users.empty?
+
+ target_users.each do |user|
+ Users::UpdateAssignedOpenIssueCountService.new(target_user: user).execute
+ end
+ rescue StandardError => exception
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
+ end
+ end
+end
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 2e3feb1a4d1..525a72e02ef 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -2,6 +2,8 @@
class WaitForClusterCreationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include ClusterQueue
worker_has_external_dependencies!
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index 5230f3bfa1f..dffab61dd0e 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class WebHookWorker # rubocop:disable Scalability/IdempotentWorker
+# Worker cannot be idempotent: https://gitlab.com/gitlab-org/gitlab/-/issues/218559
+# rubocop:disable Scalability/IdempotentWorker
+class WebHookWorker
include ApplicationWorker
feature_category :integrations
@@ -16,3 +18,4 @@ class WebHookWorker # rubocop:disable Scalability/IdempotentWorker
WebHookService.new(hook, data, hook_name).execute
end
end
+# rubocop:enable Scalability/IdempotentWorker
diff --git a/app/workers/web_hooks/destroy_worker.rb b/app/workers/web_hooks/destroy_worker.rb
index 13a5a7bf1e6..c1886576c41 100644
--- a/app/workers/web_hooks/destroy_worker.rb
+++ b/app/workers/web_hooks/destroy_worker.rb
@@ -4,7 +4,10 @@ module WebHooks
class DestroyWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :integrations
+ tags :exclude_from_kubernetes
urgency :low
idempotent!
diff --git a/app/workers/wikis/git_garbage_collect_worker.rb b/app/workers/wikis/git_garbage_collect_worker.rb
index 1b455c50618..f34d3be51d2 100644
--- a/app/workers/wikis/git_garbage_collect_worker.rb
+++ b/app/workers/wikis/git_garbage_collect_worker.rb
@@ -5,6 +5,8 @@ module Wikis
extend ::Gitlab::Utils::Override
include GitGarbageCollectMethods
+ tags :exclude_from_kubernetes
+
private
override :find_resource
diff --git a/app/workers/x509_certificate_revoke_worker.rb b/app/workers/x509_certificate_revoke_worker.rb
index abd0e5eefa7..cbf9fbb7525 100644
--- a/app/workers/x509_certificate_revoke_worker.rb
+++ b/app/workers/x509_certificate_revoke_worker.rb
@@ -3,6 +3,8 @@
class X509CertificateRevokeWorker
include ApplicationWorker
+ sidekiq_options retry: 3
+
feature_category :source_code_management
idempotent!
diff --git a/app/workers/x509_issuer_crl_check_worker.rb b/app/workers/x509_issuer_crl_check_worker.rb
index 5fc92da803c..d66ad6c1c15 100644
--- a/app/workers/x509_issuer_crl_check_worker.rb
+++ b/app/workers/x509_issuer_crl_check_worker.rb
@@ -2,6 +2,8 @@
class X509IssuerCrlCheckWorker
include ApplicationWorker
+
+ sidekiq_options retry: 3
include CronjobQueue
feature_category :source_code_management