summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-19 09:08:42 +0000
commitb76ae638462ab0f673e5915986070518dd3f9ad3 (patch)
treebdab0533383b52873be0ec0eb4d3c66598ff8b91 /app
parent434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff)
downloadgitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/cluster_app_logos/cert_manager.pngbin1287 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/crossplane.pngbin1850 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/elastic_stack.pngbin2919 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/elasticsearch.pngbin796 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/gitlab.pngbin1757 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/helm.pngbin1438 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/jeager.pngbin2619 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/jupyterhub.pngbin895 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/knative.pngbin11259 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/kubernetes.pngbin1437 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/meltano.pngbin580 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/modsecurity.pngbin6235 -> 0 bytes
-rw-r--r--app/assets/images/cluster_app_logos/prometheus.pngbin923 -> 0 bytes
-rw-r--r--app/assets/images/dev_ops_report_overview.svg (renamed from app/views/shared/icons/_dev_ops_report_overview.svg)0
-rw-r--r--app/assets/images/gitorious-logo-black.pngbin631 -> 0 bytes
-rw-r--r--app/assets/images/gitorious-logo-blue.pngbin201 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/admin_verify-0.pngbin0 -> 30421 bytes
-rw-r--r--app/assets/javascripts/admin/users/components/actions/ban.vue7
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue1
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue1
-rw-r--r--app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue6
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score.vue88
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue55
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue14
-rw-r--r--app/assets/javascripts/analytics/devops_report/constants.js11
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score.js4
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js8
-rw-r--r--app/assets/javascripts/api/analytics_api.js72
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue6
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue11
-rw-r--r--app/assets/javascripts/blob/csv/csv_viewer.vue13
-rw-r--r--app/assets/javascripts/blob/viewer/index.js62
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js36
-rw-r--r--app/assets/javascripts/boards/boards_util.js3
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue77
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue30
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue48
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue109
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_item.vue95
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue57
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue5
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue21
-rw-r--r--app/assets/javascripts/boards/constants.js7
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js128
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js33
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue23
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue3
-rw-r--r--app/assets/javascripts/commons/vue.js2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue108
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_error.vue31
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue24
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue40
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue67
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue58
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_image_button.vue19
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_link_button.vue110
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue50
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue60
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue53
-rw-r--r--app/assets/javascripts/content_editor/constants.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/attachment.js53
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/bold.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/bullet_list.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/code.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/document.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/dropcursor.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js93
-rw-r--r--app/assets/javascripts/content_editor/extensions/gapcursor.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/hard_break.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/heading.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/history.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/horizontal_rule.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js103
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js50
-rw-r--r--app/assets/javascripts/content_editor/extensions/italic.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/list_item.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/loading.js24
-rw-r--r--app/assets/javascripts/content_editor/extensions/ordered_list.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/paragraph.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js78
-rw-r--r--app/assets/javascripts/content_editor/extensions/strike.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/subscript.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/superscript.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/table.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_row.js48
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_item.js33
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_list.js30
-rw-r--r--app/assets/javascripts/content_editor/extensions/text.js6
-rw-r--r--app/assets/javascripts/content_editor/services/build_serializer_config.js22
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js42
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js78
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js174
-rw-r--r--app/assets/javascripts/content_editor/services/track_ui_control.js9
-rw-r--r--app/assets/javascripts/content_editor/services/upload_file.js44
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js123
-rw-r--r--app/assets/javascripts/content_editor/services/utils.js14
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue228
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue9
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.vue56
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_component.vue54
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue60
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue70
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue60
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue266
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.vue56
-rw-r--r--app/assets/javascripts/cycle_analytics/components/total_time_component.vue50
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue107
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js59
-rw-r--r--app/assets/javascripts/cycle_analytics/default_event_objects.js98
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js9
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js101
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js63
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js9
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js68
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js1
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue20
-rw-r--r--app/assets/javascripts/design_management/components/image.vue14
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue30
-rw-r--r--app/assets/javascripts/design_management/graphql.js8
-rw-r--r--app/assets/javascripts/design_management/graphql/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_todo_item.fragment.graphql11
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql6
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql6
-rw-r--r--app/assets/javascripts/design_management/index.js12
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue20
-rw-r--r--app/assets/javascripts/diffs/components/app.vue94
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue99
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue19
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue4
-rw-r--r--app/assets/javascripts/diffs/components/settings_dropdown.vue4
-rw-r--r--app/assets/javascripts/diffs/constants.js2
-rw-r--r--app/assets/javascripts/diffs/i18n.js22
-rw-r--r--app/assets/javascripts/diffs/index.js16
-rw-r--r--app/assets/javascripts/diffs/store/actions.js50
-rw-r--r--app/assets/javascripts/diffs/store/getters.js22
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js3
-rw-r--r--app/assets/javascripts/diffs/utils/queue_events.js13
-rw-r--r--app/assets/javascripts/editor/constants.js6
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js159
-rw-r--r--app/assets/javascripts/editor/source_editor.js29
-rw-r--r--app/assets/javascripts/editor/utils.js31
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue135
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue58
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue146
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue36
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue36
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue174
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue51
-rw-r--r--app/assets/javascripts/environments/components/rollback_modal_manager.vue57
-rw-r--r--app/assets/javascripts/environments/constants.js2
-rw-r--r--app/assets/javascripts/environments/edit.js18
-rw-r--r--app/assets/javascripts/environments/init_confirm_rollback_modal.js16
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js14
-rw-r--r--app/assets/javascripts/environments/mount_show.js38
-rw-r--r--app/assets/javascripts/environments/new.js11
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue8
-rw-r--r--app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js36
-rw-r--r--app/assets/javascripts/frequent_items/index.js77
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql8
-rw-r--r--app/assets/javascripts/graphql_shared/queries/get_users_projects.query.graphql28
-rw-r--r--app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql15
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js19
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue17
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js15
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue15
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue27
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue269
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue227
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue162
-rw-r--r--app/assets/javascripts/import_entities/import_groups/constants.js7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js68
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql13
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql8
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql8
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql (renamed from app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql)0
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/utils.js13
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue18
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/integrations/overrides/api.js10
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue127
-rw-r--r--app/assets/javascripts/integrations/overrides/index.js23
-rw-r--r--app/assets/javascripts/invite_members/components/group_select.vue1
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue95
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue25
-rw-r--r--app/assets/javascripts/invite_members/constants.js7
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js4
-rw-r--r--app/assets/javascripts/issuable_form.js17
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue26
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_tabs.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/type.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue2
-rw-r--r--app/assets/javascripts/issue_show/constants.js4
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue6
-rw-r--r--app/assets/javascripts/issues_list/components/issue_card_time_info.vue6
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue56
-rw-r--r--app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue8
-rw-r--r--app/assets/javascripts/issues_list/constants.js31
-rw-r--r--app/assets/javascripts/issues_list/index.js43
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql4
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql4
-rw-r--r--app/assets/javascripts/issues_list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql7
-rw-r--r--app/assets/javascripts/issues_list/utils.js24
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue174
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue4
-rw-r--r--app/assets/javascripts/jira_connect/branches/constants.js23
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql6
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js36
-rw-r--r--app/assets/javascripts/jira_connect/branches/pages/index.vue60
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/.eslintrc.yml (renamed from app/assets/javascripts/jira_connect/.eslintrc.yml)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/api.js (renamed from app/assets/javascripts/jira_connect/api.js)2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue (renamed from app/assets/javascripts/jira_connect/components/app.vue)2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/group_item_name.vue (renamed from app/assets/javascripts/jira_connect/components/group_item_name.vue)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue (renamed from app/assets/javascripts/jira_connect/components/groups_list.vue)7
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue (renamed from app/assets/javascripts/jira_connect/components/groups_list_item.vue)4
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue (renamed from app/assets/javascripts/jira_connect/components/subscriptions_list.vue)4
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js (renamed from app/assets/javascripts/jira_connect/constants.js)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/index.js (renamed from app/assets/javascripts/jira_connect/index.js)4
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/index.js (renamed from app/assets/javascripts/jira_connect/store/index.js)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js (renamed from app/assets/javascripts/jira_connect/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/mutations.js (renamed from app/assets/javascripts/jira_connect/store/mutations.js)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/store/state.js (renamed from app/assets/javascripts/jira_connect/store/state.js)0
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/utils.js (renamed from app/assets/javascripts/jira_connect/utils.js)0
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue2
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue13
-rw-r--r--app/assets/javascripts/jobs/store/actions.js14
-rw-r--r--app/assets/javascripts/jobs/store/utils.js23
-rw-r--r--app/assets/javascripts/jobs/utils.js9
-rw-r--r--app/assets/javascripts/lazy_loader.js2
-rw-r--r--app/assets/javascripts/lib/dompurify.js17
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js25
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js16
-rw-r--r--app/assets/javascripts/main.js229
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue35
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue25
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue22
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue (renamed from app/assets/javascripts/vue_shared/components/remove_member_modal.vue)72
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue3
-rw-r--r--app/assets/javascripts/members/index.js10
-rw-r--r--app/assets/javascripts/members/store/actions.js8
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/members/store/mutations.js7
-rw-r--r--app/assets/javascripts/members/store/state.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js29
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue13
-rw-r--r--app/assets/javascripts/nav/components/responsive_app.vue14
-rw-r--r--app/assets/javascripts/nav/event_hub.js5
-rw-r--r--app/assets/javascripts/nav/index.js5
-rw-r--r--app/assets/javascripts/nav/utils/has_menu_expanded.js2
-rw-r--r--app/assets/javascripts/nav/utils/index.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue1
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue5
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue5
-rw-r--r--app/assets/javascripts/notes/index.js9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue106
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue248
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue87
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue79
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue38
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue41
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue45
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue38
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue229
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue141
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue75
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue163
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue169
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue134
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue93
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue71
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js88
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json17
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js23
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql111
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.js5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/utils.js40
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue64
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js26
-rw-r--r--app/assets/javascripts/pages/admin/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js22
-rw-r--r--app/assets/javascripts/pages/admin/integrations/overrides/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js18
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js18
-rw-r--r--app/assets/javascripts/pages/admin/serverless/domains/index.js28
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js16
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/index/index.js2
-rw-r--r--app/assets/javascripts/pages/explore/groups/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js17
-rw-r--r--app/assets/javascripts/pages/groups/runners/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/badges/index.js4
-rw-r--r--app/assets/javascripts/pages/help/index/index.js6
-rw-r--r--app/assets/javascripts/pages/help/ui/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bitbucket/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js2
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/gitea/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/github/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/gitlab/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/gitlab_projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/import/manifest/status/index.js6
-rw-r--r--app/assets/javascripts/pages/jira_connect/branches/new/index.js3
-rw-r--r--app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js2
-rw-r--r--app/assets/javascripts/pages/omniauth_callbacks/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/accounts/show/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js8
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js18
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/file/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/cycle_analytics/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/environments/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/folder/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/metrics/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/environments/terminal/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js268
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/import/jira/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/terminal/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/network/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js17
-rw-r--r--app/assets/javascripts/pages/projects/security/configuration/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js20
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue106
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js2
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/static_site_editor/show/index.js4
-rw-r--r--app/assets/javascripts/pages/sessions/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js24
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue75
-rw-r--r--app/assets/javascripts/performance/constants.js11
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue2
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue89
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue269
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue48
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue91
-rw-r--r--app/assets/javascripts/pipelines/components/graph/perf_utils.js50
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue112
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js26
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue117
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue48
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue31
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue144
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue106
-rw-r--r--app/assets/javascripts/pipelines/constants.js16
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js65
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js66
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js81
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js21
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js206
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue26
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue94
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql32
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js27
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/compare/components/app_legacy.vue112
-rw-r--r--app/assets/javascripts/projects/compare/index.js43
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue2
-rw-r--r--app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue42
-rw-r--r--app/assets/javascripts/projects/terraform_notification/index.js13
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue27
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue23
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js11
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql1
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue4
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue11
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js12
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js4
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js4
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/state.js3
-rw-r--r--app/assets/javascripts/reports/constants.js1
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/components/modal.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue29
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue34
-rw-r--r--app/assets/javascripts/repository/components/blob_edit.vue22
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue3
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue99
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue1
-rw-r--r--app/assets/javascripts/repository/constants.js3
-rw-r--r--app/assets/javascripts/repository/mutations/lock_path.mutation.graphql13
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql11
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue (renamed from app/assets/javascripts/runner/runner_list/runner_list_app.vue)6
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js (renamed from app/assets/javascripts/runner/runner_list/index.js)6
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue41
-rw-r--r--app/assets/javascripts/runner/components/runner_type_alert.vue17
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue8
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue6
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue35
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js40
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js (renamed from app/assets/javascripts/runner/runner_list/runner_search_utils.js)2
-rw-r--r--app/assets/javascripts/search/store/actions.js36
-rw-r--r--app/assets/javascripts/search/store/utils.js4
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue5
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue14
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue9
-rw-r--r--app/assets/javascripts/search_autocomplete.js8
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue211
-rw-r--r--app/assets/javascripts/security_configuration/components/auto_dev_ops_enabled_alert.vue30
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue109
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js80
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue14
-rw-r--r--app/assets/javascripts/security_configuration/components/redesigned_app.vue179
-rw-r--r--app/assets/javascripts/security_configuration/components/section_layout.vue2
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade.vue32
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade_banner.vue22
-rw-r--r--app/assets/javascripts/security_configuration/index.js41
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue36
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue37
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue10
-rw-r--r--app/assets/javascripts/sidebar/constants.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js7
-rw-r--r--app/assets/javascripts/sidebar/queries/group_milestones.query.graphql20
-rw-r--r--app/assets/javascripts/sidebar/queries/milestone.fragment.graphql1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue9
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue35
-rw-r--r--app/assets/javascripts/snippets/index.js10
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js15
-rw-r--r--app/assets/javascripts/sourcegraph/load.js5
-rw-r--r--app/assets/javascripts/syntax_highlight.js36
-rw-r--r--app/assets/javascripts/terraform/components/empty_state.vue30
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue86
-rw-r--r--app/assets/javascripts/terraform/components/states_table_actions.vue30
-rw-r--r--app/assets/javascripts/terraform/index.js7
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue7
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue1
-rw-r--r--app/assets/javascripts/tracking/constants.js25
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js23
-rw-r--r--app/assets/javascripts/tracking/index.js266
-rw-r--r--app/assets/javascripts/tracking/tracking.js193
-rw-r--r--app/assets/javascripts/tracking/utils.js102
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue56
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue32
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/auto_merge.js11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue165
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue100
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue99
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue235
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/papa_parse_alert.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js20
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue260
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_date.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue36
-rw-r--r--app/assets/javascripts/vue_shared/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue12
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/provider.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql (renamed from app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql10
-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.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/utils.js2
-rw-r--r--app/assets/stylesheets/_jh/application_jh.scss5
-rw-r--r--app/assets/stylesheets/application.scss3
-rw-r--r--app/assets/stylesheets/components/content_editor.scss56
-rw-r--r--app/assets/stylesheets/components/whats_new.scss10
-rw-r--r--app/assets/stylesheets/framework/animations.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/framework/diffs.scss16
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss61
-rw-r--r--app/assets/stylesheets/framework/files.scss56
-rw-r--r--app/assets/stylesheets/framework/header.scss50
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss35
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss1
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/source_editor.scss15
-rw-r--r--app/assets/stylesheets/lazy_bundles/select2_overrides.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss21
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss1
-rw-r--r--app/assets/stylesheets/pages/events.scss15
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss4
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss23
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss21
-rw-r--r--app/assets/stylesheets/themes/_dark.scss18
-rw-r--r--app/assets/stylesheets/utilities.scss23
-rw-r--r--app/controllers/admin/clusters/applications_controller.rb11
-rw-r--r--app/controllers/admin/integrations_controller.rb17
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb8
-rw-r--r--app/controllers/admin/services_controller.rb47
-rw-r--r--app/controllers/admin/users_controller.rb11
-rw-r--r--app/controllers/clusters/applications_controller.rb56
-rw-r--r--app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb96
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb34
-rw-r--r--app/controllers/concerns/dependency_proxy/auth.rb43
-rw-r--r--app/controllers/concerns/dependency_proxy/group_access.rb6
-rw-r--r--app/controllers/concerns/find_snippet.rb6
-rw-r--r--app/controllers/concerns/integrations_actions.rb3
-rw-r--r--app/controllers/concerns/issuable_actions.rb50
-rw-r--r--app/controllers/concerns/lfs_request.rb15
-rw-r--r--app/controllers/concerns/spammable_actions.rb73
-rw-r--r--app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb28
-rw-r--r--app/controllers/concerns/spammable_actions/attributes.rb13
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/common.rb23
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb35
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb25
-rw-r--r--app/controllers/customers_dot/proxy_controller.rb21
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb8
-rw-r--r--app/controllers/groups/boards_controller.rb2
-rw-r--r--app/controllers/groups/clusters/applications_controller.rb18
-rw-r--r--app/controllers/groups/dependency_proxies_controller.rb2
-rw-r--r--app/controllers/groups/dependency_proxy/application_controller.rb66
-rw-r--r--app/controllers/groups/dependency_proxy_auth_controller.rb4
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb27
-rw-r--r--app/controllers/groups/email_campaigns_controller.rb6
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups/runners_controller.rb13
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb5
-rw-r--r--app/controllers/groups/settings/integrations_controller.rb4
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/import/available_namespaces_controller.rb2
-rw-r--r--app/controllers/import/base_controller.rb11
-rw-r--r--app/controllers/invites_controller.rb12
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb40
-rw-r--r--app/controllers/jira_connect/branches_controller.rb29
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb7
-rw-r--r--app/controllers/jwt_controller.rb7
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb1
-rw-r--r--app/controllers/projects/analytics/cycle_analytics/stages_controller.rb37
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb15
-rw-r--r--app/controllers/projects/compare_controller.rb5
-rw-r--r--app/controllers/projects/environments_controller.rb8
-rw-r--r--app/controllers/projects/error_tracking_controller.rb10
-rw-r--r--app/controllers/projects/forks_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb13
-rw-r--r--app/controllers/projects/jobs_controller.rb26
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb38
-rw-r--r--app/controllers/projects/merge_requests_controller.rb23
-rw-r--r--app/controllers/projects/packages/infrastructure_registry_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_controller.rb18
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/raw_controller.rb2
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/security/configuration_controller.rb4
-rw-r--r--app/controllers/projects/services_controller.rb25
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/registrations/welcome_controller.rb8
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/controllers/repositories/git_http_client_controller.rb2
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb6
-rw-r--r--app/controllers/repositories/lfs_locks_api_controller.rb4
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb4
-rw-r--r--app/controllers/runner_setup_controller.rb2
-rw-r--r--app/controllers/search_controller.rb16
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/experiments/application_experiment.rb8
-rw-r--r--app/experiments/force_company_trial_experiment.rb11
-rw-r--r--app/experiments/members/invite_email_experiment.rb93
-rw-r--r--app/experiments/new_project_readme_content_experiment.rb2
-rw-r--r--app/experiments/new_project_readme_experiment.rb38
-rw-r--r--app/finders/alert_management/alerts_finder.rb2
-rw-r--r--app/finders/ci/pipelines_finder.rb10
-rw-r--r--app/finders/error_tracking/errors_finder.rb37
-rw-r--r--app/finders/group_members_finder.rb9
-rw-r--r--app/finders/groups_finder.rb10
-rw-r--r--app/finders/issuable_finder.rb3
-rw-r--r--app/finders/issuable_finder/params.rb28
-rw-r--r--app/finders/issues_finder.rb20
-rw-r--r--app/finders/issues_finder/params.rb8
-rw-r--r--app/finders/lfs_pointers_finder.rb36
-rw-r--r--app/finders/merge_requests_finder.rb13
-rw-r--r--app/finders/packages/pypi/packages_finder.rb7
-rw-r--r--app/finders/projects/members/effective_access_level_finder.rb10
-rw-r--r--app/finders/projects/members/effective_access_level_per_user_finder.rb20
-rw-r--r--app/finders/security/jobs_finder.rb19
-rw-r--r--app/graphql/gitlab_schema.rb24
-rw-r--r--app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb6
-rw-r--r--app/graphql/mutations/alert_management/alerts/set_assignees.rb6
-rw-r--r--app/graphql/mutations/alert_management/base.rb14
-rw-r--r--app/graphql/mutations/alert_management/http_integration/create.rb10
-rw-r--r--app/graphql/mutations/alert_management/http_integration/destroy.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/reset_token.rb2
-rw-r--r--app/graphql/mutations/alert_management/http_integration/update.rb8
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/create.rb8
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb2
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb2
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/update.rb6
-rw-r--r--app/graphql/mutations/alert_management/update_alert_status.rb2
-rw-r--r--app/graphql/mutations/award_emojis/base.rb6
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb2
-rw-r--r--app/graphql/mutations/base_mutation.rb8
-rw-r--r--app/graphql/mutations/boards/common_mutation_arguments.rb8
-rw-r--r--app/graphql/mutations/boards/create.rb2
-rw-r--r--app/graphql/mutations/boards/destroy.rb4
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb18
-rw-r--r--app/graphql/mutations/boards/lists/base_create.rb2
-rw-r--r--app/graphql/mutations/boards/lists/base_update.rb4
-rw-r--r--app/graphql/mutations/boards/lists/destroy.rb2
-rw-r--r--app/graphql/mutations/boards/update.rb4
-rw-r--r--app/graphql/mutations/branches/create.rb6
-rw-r--r--app/graphql/mutations/ci/ci_cd_settings_update.rb8
-rw-r--r--app/graphql/mutations/ci/job/base.rb2
-rw-r--r--app/graphql/mutations/ci/job/cancel.rb28
-rw-r--r--app/graphql/mutations/ci/job/play.rb2
-rw-r--r--app/graphql/mutations/ci/job/retry.rb2
-rw-r--r--app/graphql/mutations/ci/job/unschedule.rb28
-rw-r--r--app/graphql/mutations/ci/job_token_scope/add_project.rb10
-rw-r--r--app/graphql/mutations/ci/job_token_scope/remove_project.rb10
-rw-r--r--app/graphql/mutations/ci/pipeline/base.rb2
-rw-r--r--app/graphql/mutations/ci/pipeline/retry.rb2
-rw-r--r--app/graphql/mutations/ci/runner/update.rb14
-rw-r--r--app/graphql/mutations/ci/runners_registration_token/reset.rb6
-rw-r--r--app/graphql/mutations/commits/create.rb14
-rw-r--r--app/graphql/mutations/concerns/mutations/assignable.rb6
-rw-r--r--app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb20
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb4
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_subscription.rb4
-rw-r--r--app/graphql/mutations/container_expiration_policies/update.rb8
-rw-r--r--app/graphql/mutations/container_repositories/destroy.rb2
-rw-r--r--app/graphql/mutations/container_repositories/destroy_tags.rb4
-rw-r--r--app/graphql/mutations/custom_emoji/create.rb8
-rw-r--r--app/graphql/mutations/design_management/base.rb8
-rw-r--r--app/graphql/mutations/design_management/delete.rb6
-rw-r--r--app/graphql/mutations/design_management/move.rb2
-rw-r--r--app/graphql/mutations/design_management/upload.rb4
-rw-r--r--app/graphql/mutations/discussions/toggle_resolve.rb6
-rw-r--r--app/graphql/mutations/echo.rb6
-rw-r--r--app/graphql/mutations/environments/canary_ingress/update.rb6
-rw-r--r--app/graphql/mutations/groups/update.rb40
-rw-r--r--app/graphql/mutations/issues/base.rb10
-rw-r--r--app/graphql/mutations/issues/common_mutation_arguments.rb6
-rw-r--r--app/graphql/mutations/issues/create.rb24
-rw-r--r--app/graphql/mutations/issues/move.rb4
-rw-r--r--app/graphql/mutations/issues/set_confidential.rb2
-rw-r--r--app/graphql/mutations/issues/set_due_date.rb13
-rw-r--r--app/graphql/mutations/issues/set_locked.rb2
-rw-r--r--app/graphql/mutations/issues/set_subscription.rb10
-rw-r--r--app/graphql/mutations/issues/update.rb46
-rw-r--r--app/graphql/mutations/jira_import/import_users.rb8
-rw-r--r--app/graphql/mutations/jira_import/start.rb12
-rw-r--r--app/graphql/mutations/labels/create.rb8
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb12
-rw-r--r--app/graphql/mutations/merge_requests/base.rb10
-rw-r--r--app/graphql/mutations/merge_requests/create.rb14
-rw-r--r--app/graphql/mutations/merge_requests/reviewer_rereview.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_draft.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_labels.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_locked.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_milestone.rb2
-rw-r--r--app/graphql/mutations/merge_requests/set_subscription.rb10
-rw-r--r--app/graphql/mutations/merge_requests/set_wip.rb2
-rw-r--r--app/graphql/mutations/merge_requests/update.rb8
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/create.rb14
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb10
-rw-r--r--app/graphql/mutations/notes/base.rb2
-rw-r--r--app/graphql/mutations/notes/create/base.rb8
-rw-r--r--app/graphql/mutations/notes/create/note.rb2
-rw-r--r--app/graphql/mutations/notes/destroy.rb2
-rw-r--r--app/graphql/mutations/notes/reposition_image_diff_note.rb2
-rw-r--r--app/graphql/mutations/notes/update/base.rb2
-rw-r--r--app/graphql/mutations/notes/update/image_diff_note.rb2
-rw-r--r--app/graphql/mutations/notes/update/note.rb6
-rw-r--r--app/graphql/mutations/packages/destroy_file.rb35
-rw-r--r--app/graphql/mutations/release_asset_links/create.rb6
-rw-r--r--app/graphql/mutations/release_asset_links/delete.rb2
-rw-r--r--app/graphql/mutations/release_asset_links/update.rb10
-rw-r--r--app/graphql/mutations/releases/base.rb2
-rw-r--r--app/graphql/mutations/releases/create.rb18
-rw-r--r--app/graphql/mutations/releases/delete.rb4
-rw-r--r--app/graphql/mutations/releases/update.rb14
-rw-r--r--app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb6
-rw-r--r--app/graphql/mutations/snippets/base.rb2
-rw-r--r--app/graphql/mutations/snippets/create.rb16
-rw-r--r--app/graphql/mutations/snippets/destroy.rb2
-rw-r--r--app/graphql/mutations/snippets/mark_as_spam.rb4
-rw-r--r--app/graphql/mutations/snippets/update.rb8
-rw-r--r--app/graphql/mutations/todos/create.rb4
-rw-r--r--app/graphql/mutations/todos/mark_done.rb4
-rw-r--r--app/graphql/mutations/todos/restore.rb4
-rw-r--r--app/graphql/mutations/todos/restore_many.rb2
-rw-r--r--app/graphql/mutations/user_callouts/create.rb6
-rw-r--r--app/graphql/queries/design_management/get_design_list.query.graphql7
-rw-r--r--app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb2
-rw-r--r--app/graphql/resolvers/alert_management/alert_resolver.rb6
-rw-r--r--app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb4
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb6
-rw-r--r--app/graphql/resolvers/board_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb10
-rw-r--r--app/graphql/resolvers/ci/runner_setup_resolver.rb4
-rw-r--r--app/graphql/resolvers/ci/runners_resolver.rb4
-rw-r--r--app/graphql/resolvers/ci/template_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/test_suite_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/group_issuable_resolver.rb2
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb39
-rw-r--r--app/graphql/resolvers/concerns/resolves_ids.rb16
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb4
-rw-r--r--app/graphql/resolvers/concerns/resolves_snippets.rb16
-rw-r--r--app/graphql/resolvers/container_repositories_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/design_at_version_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/design_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/designs_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/version/design_at_version_resolver.rb8
-rw-r--r--app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/version_in_collection_resolver.rb6
-rw-r--r--app/graphql/resolvers/design_management/version_resolver.rb2
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb6
-rw-r--r--app/graphql/resolvers/echo_resolver.rb4
-rw-r--r--app/graphql/resolvers/environments_resolver.rb6
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb4
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_labels_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_milestones_resolver.rb4
-rw-r--r--app/graphql/resolvers/groups_resolver.rb39
-rw-r--r--app/graphql/resolvers/issues_resolver.rb3
-rw-r--r--app/graphql/resolvers/labels_resolver.rb4
-rw-r--r--app/graphql/resolvers/members_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb2
-rw-r--r--app/graphql/resolvers/merge_requests_count_resolver.rb17
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb20
-rw-r--r--app/graphql/resolvers/metrics/dashboard_resolver.rb2
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb6
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb6
-rw-r--r--app/graphql/resolvers/packages_base_resolver.rb4
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb47
-rw-r--r--app/graphql/resolvers/project_milestones_resolver.rb2
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb4
-rw-r--r--app/graphql/resolvers/projects/jira_projects_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects/services_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects_resolver.rb12
-rw-r--r--app/graphql/resolvers/release_resolver.rb2
-rw-r--r--app/graphql/resolvers/repository_branch_names_resolver.rb8
-rw-r--r--app/graphql/resolvers/snippets/blobs_resolver.rb2
-rw-r--r--app/graphql/resolvers/snippets_resolver.rb3
-rw-r--r--app/graphql/resolvers/terraform/states_resolver.rb2
-rw-r--r--app/graphql/resolvers/timelog_resolver.rb101
-rw-r--r--app/graphql/resolvers/todo_resolver.rb6
-rw-r--r--app/graphql/resolvers/tree_resolver.rb6
-rw-r--r--app/graphql/resolvers/user_discussions_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/user_merge_requests_resolver_base.rb2
-rw-r--r--app/graphql/resolvers/user_notes_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/user_resolver.rb2
-rw-r--r--app/graphql/resolvers/user_starred_projects_resolver.rb2
-rw-r--r--app/graphql/resolvers/users/group_count_resolver.rb2
-rw-r--r--app/graphql/resolvers/users_resolver.rb8
-rw-r--r--app/graphql/types/access_level_type.rb2
-rw-r--r--app/graphql/types/admin/analytics/usage_trends/measurement_type.rb2
-rw-r--r--app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb6
-rw-r--r--app/graphql/types/alert_management/alert_status_counts_type.rb6
-rw-r--r--app/graphql/types/alert_management/alert_type.rb22
-rw-r--r--app/graphql/types/alert_management/integration_type.rb12
-rw-r--r--app/graphql/types/award_emojis/award_emoji_type.rb10
-rw-r--r--app/graphql/types/base_argument.rb22
-rw-r--r--app/graphql/types/blob_viewer_type.rb12
-rw-r--r--app/graphql/types/board_list_type.rb12
-rw-r--r--app/graphql/types/board_type.rb12
-rw-r--r--app/graphql/types/boards/board_issuable_input_base_type.rb6
-rw-r--r--app/graphql/types/boards/board_issue_input_base_type.rb13
-rw-r--r--app/graphql/types/boards/board_issue_input_type.rb2
-rw-r--r--app/graphql/types/branch_type.rb2
-rw-r--r--app/graphql/types/ci/analytics_type.rb22
-rw-r--r--app/graphql/types/ci/application_setting_type.rb2
-rw-r--r--app/graphql/types/ci/build_need_type.rb4
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb8
-rw-r--r--app/graphql/types/ci/config/config_type.rb4
-rw-r--r--app/graphql/types/ci/config/group_type.rb4
-rw-r--r--app/graphql/types/ci/config/job_restriction_type.rb2
-rw-r--r--app/graphql/types/ci/config/job_type.rb20
-rw-r--r--app/graphql/types/ci/config/need_type.rb2
-rw-r--r--app/graphql/types/ci/config/stage_type.rb2
-rw-r--r--app/graphql/types/ci/detailed_status_type.rb18
-rw-r--r--app/graphql/types/ci/group_type.rb6
-rw-r--r--app/graphql/types/ci/job_artifact_type.rb2
-rw-r--r--app/graphql/types/ci/job_type.rb34
-rw-r--r--app/graphql/types/ci/pipeline_type.rb30
-rw-r--r--app/graphql/types/ci/recent_failures_type.rb4
-rw-r--r--app/graphql/types/ci/runner_architecture_type.rb4
-rw-r--r--app/graphql/types/ci/runner_platform_type.rb4
-rw-r--r--app/graphql/types/ci/runner_setup_type.rb4
-rw-r--r--app/graphql/types/ci/runner_type.rb25
-rw-r--r--app/graphql/types/ci/stage_type.rb16
-rw-r--r--app/graphql/types/ci/status_action_type.rb12
-rw-r--r--app/graphql/types/ci/template_type.rb4
-rw-r--r--app/graphql/types/ci/test_case_type.rb12
-rw-r--r--app/graphql/types/ci/test_report_total_type.rb12
-rw-r--r--app/graphql/types/ci/test_suite_summary_type.rb16
-rw-r--r--app/graphql/types/ci/test_suite_type.rb14
-rw-r--r--app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb4
-rw-r--r--app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb8
-rw-r--r--app/graphql/types/ci_configuration/sast/entity_input_type.rb6
-rw-r--r--app/graphql/types/ci_configuration/sast/entity_type.rb12
-rw-r--r--app/graphql/types/ci_configuration/sast/options_entity_type.rb4
-rw-r--r--app/graphql/types/commit_action_type.rb10
-rw-r--r--app/graphql/types/commit_type.rb22
-rw-r--r--app/graphql/types/container_expiration_policy_type.rb2
-rw-r--r--app/graphql/types/container_repository_tag_type.rb14
-rw-r--r--app/graphql/types/container_repository_type.rb12
-rw-r--r--app/graphql/types/countable_connection_type.rb2
-rw-r--r--app/graphql/types/custom_emoji_type.rb6
-rw-r--r--app/graphql/types/design_management/design_fields.rb12
-rw-r--r--app/graphql/types/design_management/version_type.rb4
-rw-r--r--app/graphql/types/diff_paths_input_type.rb4
-rw-r--r--app/graphql/types/diff_refs_type.rb6
-rw-r--r--app/graphql/types/diff_stats_summary_type.rb8
-rw-r--r--app/graphql/types/diff_stats_type.rb6
-rw-r--r--app/graphql/types/environment_type.rb8
-rw-r--r--app/graphql/types/error_tracking/sentry_detailed_error_type.rb46
-rw-r--r--app/graphql/types/error_tracking/sentry_error_collection_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_frequency_type.rb2
-rw-r--r--app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb4
-rw-r--r--app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb8
-rw-r--r--app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb4
-rw-r--r--app/graphql/types/error_tracking/sentry_error_tags_type.rb4
-rw-r--r--app/graphql/types/error_tracking/sentry_error_type.rb26
-rw-r--r--app/graphql/types/event_type.rb2
-rw-r--r--app/graphql/types/evidence_type.rb6
-rw-r--r--app/graphql/types/grafana_integration_type.rb6
-rw-r--r--app/graphql/types/group_type.rb30
-rw-r--r--app/graphql/types/invitation_interface.rb2
-rw-r--r--app/graphql/types/issue_status_counts_type.rb2
-rw-r--r--app/graphql/types/issue_type.rb51
-rw-r--r--app/graphql/types/issue_type_enum.rb2
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb13
-rw-r--r--app/graphql/types/jira_import_type.rb8
-rw-r--r--app/graphql/types/jira_user_type.rb12
-rw-r--r--app/graphql/types/jira_users_mapping_input_type.rb4
-rw-r--r--app/graphql/types/label_type.rb10
-rw-r--r--app/graphql/types/member_interface.rb2
-rw-r--r--app/graphql/types/merge_request_sort_enum.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb112
-rw-r--r--app/graphql/types/metadata/kas_type.rb6
-rw-r--r--app/graphql/types/metadata_type.rb4
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb4
-rw-r--r--app/graphql/types/metrics/dashboards/annotation_type.rb6
-rw-r--r--app/graphql/types/milestone_stats_type.rb4
-rw-r--r--app/graphql/types/milestone_type.rb18
-rw-r--r--app/graphql/types/milestone_wildcard_id_enum.rb13
-rw-r--r--app/graphql/types/mutation_type.rb4
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb4
-rw-r--r--app/graphql/types/namespace/shared_runners_setting_enum.rb13
-rw-r--r--app/graphql/types/namespace_type.rb23
-rw-r--r--app/graphql/types/negated_milestone_wildcard_id_enum.rb11
-rw-r--r--app/graphql/types/notes/diff_image_position_input_type.rb8
-rw-r--r--app/graphql/types/notes/diff_position_base_input_type.rb6
-rw-r--r--app/graphql/types/notes/diff_position_input_type.rb4
-rw-r--r--app/graphql/types/notes/diff_position_type.rb18
-rw-r--r--app/graphql/types/notes/note_type.rb10
-rw-r--r--app/graphql/types/notes/update_diff_image_position_input_type.rb8
-rw-r--r--app/graphql/types/packages/composer/json_type.rb8
-rw-r--r--app/graphql/types/packages/composer/metadatum_type.rb2
-rw-r--r--app/graphql/types/packages/conan/file_metadatum_type.rb6
-rw-r--r--app/graphql/types/packages/conan/metadatum_type.rb8
-rw-r--r--app/graphql/types/packages/dependency_link_metadata_type.rb23
-rw-r--r--app/graphql/types/packages/maven/metadatum_type.rb8
-rw-r--r--app/graphql/types/packages/nuget/dependency_link_metadatum_type.rb17
-rw-r--r--app/graphql/types/packages/nuget/metadatum_type.rb6
-rw-r--r--app/graphql/types/packages/package_dependency_link_type.rb40
-rw-r--r--app/graphql/types/packages/package_dependency_type.rb16
-rw-r--r--app/graphql/types/packages/package_dependency_type_enum.rb13
-rw-r--r--app/graphql/types/packages/package_details_type.rb2
-rw-r--r--app/graphql/types/packages/package_file_type.rb12
-rw-r--r--app/graphql/types/packages/package_tag_type.rb4
-rw-r--r--app/graphql/types/packages/package_type.rb4
-rw-r--r--app/graphql/types/packages/pypi/metadatum_type.rb2
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb2
-rw-r--r--app/graphql/types/project_type.rb83
-rw-r--r--app/graphql/types/projects/service_type.rb4
-rw-r--r--app/graphql/types/projects/services/jira_project_type.rb6
-rw-r--r--app/graphql/types/prometheus_alert_type.rb4
-rw-r--r--app/graphql/types/query_complexity_type.rb4
-rw-r--r--app/graphql/types/query_type.rb9
-rw-r--r--app/graphql/types/release_asset_link_shared_input_arguments.rb6
-rw-r--r--app/graphql/types/release_asset_link_type.rb12
-rw-r--r--app/graphql/types/release_assets_type.rb2
-rw-r--r--app/graphql/types/release_links_type.rb14
-rw-r--r--app/graphql/types/release_source_type.rb4
-rw-r--r--app/graphql/types/release_type.rb10
-rw-r--r--app/graphql/types/repository/blob_type.rb44
-rw-r--r--app/graphql/types/repository_type.rb14
-rw-r--r--app/graphql/types/resolvable_interface.rb4
-rw-r--r--app/graphql/types/snippet_type.rb14
-rw-r--r--app/graphql/types/snippets/blob_action_input_type.rb6
-rw-r--r--app/graphql/types/snippets/blob_type.rb22
-rw-r--r--app/graphql/types/task_completion_status.rb4
-rw-r--r--app/graphql/types/terraform/state_type.rb4
-rw-r--r--app/graphql/types/terraform/state_version_type.rb6
-rw-r--r--app/graphql/types/timelog_type.rb6
-rw-r--r--app/graphql/types/todo_type.rb4
-rw-r--r--app/graphql/types/tree/blob_type.rb8
-rw-r--r--app/graphql/types/tree/entry_type.rb10
-rw-r--r--app/graphql/types/tree/submodule_type.rb4
-rw-r--r--app/graphql/types/tree/tree_entry_type.rb4
-rw-r--r--app/graphql/types/user_interface.rb31
-rw-r--r--app/graphql/types/user_merge_request_interaction_type.rb8
-rw-r--r--app/graphql/types/user_status_type.rb4
-rw-r--r--app/helpers/admin/user_actions_helper.rb4
-rw-r--r--app/helpers/analytics/navbar_helper.rb28
-rw-r--r--app/helpers/application_helper.rb6
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/boards_helper.rb8
-rw-r--r--app/helpers/button_helper.rb6
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb4
-rw-r--r--app/helpers/clusters_helper.rb3
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/compare_helper.rb6
-rw-r--r--app/helpers/dashboard_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb8
-rw-r--r--app/helpers/emails_helper.rb34
-rw-r--r--app/helpers/environment_helper.rb27
-rw-r--r--app/helpers/environments_helper.rb8
-rw-r--r--app/helpers/groups/group_members_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb106
-rw-r--r--app/helpers/ide_helper.rb2
-rw-r--r--app/helpers/integrations_helper.rb19
-rw-r--r--app/helpers/invite_members_helper.rb64
-rw-r--r--app/helpers/issuables_description_templates_helper.rb7
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb5
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb9
-rw-r--r--app/helpers/packages_helper.rb5
-rw-r--r--app/helpers/projects/project_members_helper.rb28
-rw-r--r--app/helpers/projects/terraform_helper.rb5
-rw-r--r--app/helpers/projects_helper.rb59
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/snippets_helper.rb6
-rw-r--r--app/helpers/sorting_helper.rb10
-rw-r--r--app/helpers/sorting_titles_values_helper.rb24
-rw-r--r--app/helpers/time_zone_helper.rb5
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb13
-rw-r--r--app/helpers/users_helper.rb2
-rw-r--r--app/mailers/emails/members.rb4
-rw-r--r--app/mailers/emails/pipelines.rb3
-rw-r--r--app/mailers/emails/projects.rb23
-rw-r--r--app/mailers/previews/notify_preview.rb2
-rw-r--r--app/models/alert_management/alert.rb93
-rw-r--r--app/models/analytics/cycle_analytics/stage_event_hash.rb42
-rw-r--r--app/models/application_record.rb33
-rw-r--r--app/models/application_setting.rb22
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/ci/application_record.rb15
-rw-r--r--app/models/ci/build.rb31
-rw-r--r--app/models/ci/build_metadata.rb3
-rw-r--r--app/models/ci/build_need.rb4
-rw-r--r--app/models/ci/build_pending_state.rb4
-rw-r--r--app/models/ci/build_report_result.rb4
-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/build_trace_metadata.rb13
-rw-r--r--app/models/ci/build_trace_section.rb17
-rw-r--r--app/models/ci/build_trace_section_name.rb13
-rw-r--r--app/models/ci/ci_database_record.rb (renamed from app/models/ci/base_model.rb)2
-rw-r--r--app/models/ci/daily_build_group_report_result.rb4
-rw-r--r--app/models/ci/deleted_object.rb4
-rw-r--r--app/models/ci/freeze_period.rb6
-rw-r--r--app/models/ci/group_variable.rb3
-rw-r--r--app/models/ci/instance_variable.rb3
-rw-r--r--app/models/ci/job_artifact.rb3
-rw-r--r--app/models/ci/job_variable.rb3
-rw-r--r--app/models/ci/pending_build.rb13
-rw-r--r--app/models/ci/pipeline.rb20
-rw-r--r--app/models/ci/pipeline_artifact.rb3
-rw-r--r--app/models/ci/pipeline_chat_data.rb4
-rw-r--r--app/models/ci/pipeline_config.rb4
-rw-r--r--app/models/ci/pipeline_message.rb4
-rw-r--r--app/models/ci/pipeline_schedule.rb5
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb3
-rw-r--r--app/models/ci/pipeline_variable.rb3
-rw-r--r--app/models/ci/processable.rb6
-rw-r--r--app/models/ci/ref.rb3
-rw-r--r--app/models/ci/resource.rb19
-rw-r--r--app/models/ci/resource_group.rb4
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/ci/runner_namespace.rb4
-rw-r--r--app/models/ci/runner_project.rb4
-rw-r--r--app/models/ci/running_build.rb4
-rw-r--r--app/models/ci/sources/pipeline.rb4
-rw-r--r--app/models/ci/stage.rb3
-rw-r--r--app/models/ci/trigger.rb3
-rw-r--r--app/models/ci/trigger_request.rb4
-rw-r--r--app/models/ci/unit_test.rb4
-rw-r--r--app/models/ci/unit_test_failure.rb4
-rw-r--r--app/models/ci/variable.rb3
-rw-r--r--app/models/ci_platform_metric.rb4
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/commit.rb16
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb18
-rw-r--r--app/models/concerns/any_field_validation.rb25
-rw-r--r--app/models/concerns/cache_markdown_field.rb25
-rw-r--r--app/models/concerns/cascading_namespace_setting_attribute.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb1
-rw-r--r--app/models/concerns/ci/metadatable.rb10
-rw-r--r--app/models/concerns/ci/namespaced_model_name.rb13
-rw-r--r--app/models/concerns/counter_attribute.rb3
-rw-r--r--app/models/concerns/each_batch.rb6
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb8
-rw-r--r--app/models/concerns/expirable.rb3
-rw-r--r--app/models/concerns/has_integrations.rb12
-rw-r--r--app/models/concerns/incident_management/escalatable.rb104
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/limitable.rb2
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/packages/debian/distribution.rb19
-rw-r--r--app/models/concerns/project_features_compatibility.rb7
-rw-r--r--app/models/concerns/restricted_signup.rb52
-rw-r--r--app/models/concerns/select_for_project_authorization.rb2
-rw-r--r--app/models/concerns/sha256_attribute.rb2
-rw-r--r--app/models/concerns/sha_attribute.rb2
-rw-r--r--app/models/concerns/spammable.rb2
-rw-r--r--app/models/concerns/strip_attribute.rb8
-rw-r--r--app/models/concerns/time_trackable.rb8
-rw-r--r--app/models/concerns/timebox.rb2
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb33
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb28
-rw-r--r--app/models/concerns/x509_serial_number_attribute.rb2
-rw-r--r--app/models/customer_relations/organization.rb31
-rw-r--r--app/models/deploy_token.rb15
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/design_management/version.rb2
-rw-r--r--app/models/diff_discussion.rb6
-rw-r--r--app/models/discussion.rb9
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/error_tracking/client_key.rb22
-rw-r--r--app/models/error_tracking/error.rb69
-rw-r--r--app/models/error_tracking/error_event.rb65
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb21
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/group.rb15
-rw-r--r--app/models/group_deploy_token.rb9
-rw-r--r--app/models/hooks/web_hook.rb13
-rw-r--r--app/models/incident_management/issuable_escalation_status.rb15
-rw-r--r--app/models/instance_configuration.rb76
-rw-r--r--app/models/integration.rb43
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/datadog.rb56
-rw-r--r--app/models/integrations/irker.rb40
-rw-r--r--app/models/integrations/jenkins.rb2
-rw-r--r--app/models/integrations/jira.rb10
-rw-r--r--app/models/integrations/microsoft_teams.rb2
-rw-r--r--app/models/integrations/packagist.rb29
-rw-r--r--app/models/integrations/pushover.rb51
-rw-r--r--app/models/integrations/teamcity.rb2
-rw-r--r--app/models/integrations/unify_circuit.rb2
-rw-r--r--app/models/internal_id.rb4
-rw-r--r--app/models/issuable_severity.rb8
-rw-r--r--app/models/issue.rb56
-rw-r--r--app/models/jira_connect_installation.rb1
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/member.rb26
-rw-r--r--app/models/members/group_member.rb16
-rw-r--r--app/models/members/project_member.rb24
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb2
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/milestone.rb30
-rw-r--r--app/models/namespace.rb38
-rw-r--r--app/models/namespace_setting.rb27
-rw-r--r--app/models/namespaces/traversal/linear.rb29
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb68
-rw-r--r--app/models/namespaces/traversal/recursive.rb1
-rw-r--r--app/models/namespaces/traversal/recursive_scopes.rb36
-rw-r--r--app/models/note.rb25
-rw-r--r--app/models/notification_setting.rb6
-rw-r--r--app/models/operations/feature_flags/strategy.rb2
-rw-r--r--app/models/packages/debian.rb2
-rw-r--r--app/models/packages/event.rb8
-rw-r--r--app/models/packages/npm.rb13
-rw-r--r--app/models/packages/package.rb14
-rw-r--r--app/models/packages/package_file.rb24
-rw-r--r--app/models/personal_access_token.rb4
-rw-r--r--app/models/postgresql/detached_partition.rb7
-rw-r--r--app/models/postgresql/replication_slot.rb50
-rw-r--r--app/models/preloaders/user_max_access_level_in_projects_preloader.rb6
-rw-r--r--app/models/project.rb90
-rw-r--r--app/models/project_feature.rb25
-rw-r--r--app/models/project_setting.rb4
-rw-r--r--app/models/project_team.rb8
-rw-r--r--app/models/projects/ci_feature_usage.rb27
-rw-r--r--app/models/release_highlight.rb8
-rw-r--r--app/models/remote_mirror.rb52
-rw-r--r--app/models/repository.rb41
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/terraform/state.rb1
-rw-r--r--app/models/timelog.rb8
-rw-r--r--app/models/tree.rb6
-rw-r--r--app/models/user.rb112
-rw-r--r--app/models/user_callout.rb5
-rw-r--r--app/models/user_detail.rb1
-rw-r--r--app/models/user_interacted_project.rb12
-rw-r--r--app/models/users/banned_user.rb12
-rw-r--r--app/models/users/in_product_marketing_email.rb5
-rw-r--r--app/models/work_item/type.rb38
-rw-r--r--app/policies/group_policy.rb22
-rw-r--r--app/policies/issue_policy.rb7
-rw-r--r--app/policies/packages/dependency_link_policy.rb6
-rw-r--r--app/policies/packages/nuget/dependency_link_metadatum_policy.rb8
-rw-r--r--app/policies/project_policy.rb13
-rw-r--r--app/policies/release_policy.rb6
-rw-r--r--app/presenters/ci/build_runner_presenter.rb6
-rw-r--r--app/presenters/ci/runner_presenter.rb12
-rw-r--r--app/presenters/clusterable_presenter.rb8
-rw-r--r--app/presenters/gitlab/blame_presenter.rb7
-rw-r--r--app/presenters/group_clusterable_presenter.rb10
-rw-r--r--app/presenters/instance_clusterable_presenter.rb10
-rw-r--r--app/presenters/merge_request_presenter.rb17
-rw-r--r--app/presenters/project_clusterable_presenter.rb10
-rw-r--r--app/presenters/project_presenter.rb14
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/ci/pipeline_entity.rb1
-rw-r--r--app/serializers/concerns/diff_file_conflict_type.rb24
-rw-r--r--app/serializers/diff_file_entity.rb7
-rw-r--r--app/serializers/diff_file_metadata_entity.rb2
-rw-r--r--app/serializers/diffs_entity.rb7
-rw-r--r--app/serializers/diffs_metadata_entity.rb9
-rw-r--r--app/serializers/group_link/project_group_link_entity.rb5
-rw-r--r--app/serializers/integrations/project_entity.rb15
-rw-r--r--app/serializers/integrations/project_serializer.rb9
-rw-r--r--app/serializers/jira_connect/app_data_serializer.rb28
-rw-r--r--app/serializers/jira_connect/group_entity.rb8
-rw-r--r--app/serializers/jira_connect/subscription_entity.rb11
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb6
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb8
-rw-r--r--app/serializers/merge_request_widget_entity.rb25
-rw-r--r--app/serializers/paginated_diff_entity.rb2
-rw-r--r--app/services/admin/propagate_service_template.rb4
-rw-r--r--app/services/auth/container_registry_authentication_service.rb22
-rw-r--r--app/services/auth/dependency_proxy_authentication_service.rb20
-rw-r--r--app/services/authorized_project_update/project_recalculate_per_user_service.rb22
-rw-r--r--app/services/authorized_project_update/project_recalculate_service.rb13
-rw-r--r--app/services/auto_merge/base_service.rb6
-rw-r--r--app/services/boards/base_item_move_service.rb14
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/bulk_update_integration_service.rb13
-rw-r--r--app/services/ci/after_requeue_job_service.rb2
-rw-r--r--app/services/ci/append_build_trace_service.rb3
-rw-r--r--app/services/ci/build_cancel_service.rb35
-rw-r--r--app/services/ci/build_unschedule_service.rb35
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb5
-rw-r--r--app/services/ci/create_pipeline_service.rb18
-rw-r--r--app/services/ci/daily_build_group_report_result_service.rb8
-rw-r--r--app/services/ci/delete_unit_tests_service.rb2
-rw-r--r--app/services/ci/destroy_pipeline_service.rb2
-rw-r--r--app/services/ci/drop_pipeline_service.rb13
-rw-r--r--app/services/ci/external_pull_requests/create_pipeline_service.rb11
-rw-r--r--app/services/ci/extract_sections_from_build_trace_service.rb34
-rw-r--r--app/services/ci/pipeline_trigger_service.rb8
-rw-r--r--app/services/ci/pipelines/add_job_service.rb30
-rw-r--r--app/services/ci/queue/builds_table_strategy.rb8
-rw-r--r--app/services/ci/queue/pending_builds_strategy.rb58
-rw-r--r--app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb10
-rw-r--r--app/services/ci/unlock_artifacts_service.rb2
-rw-r--r--app/services/dependency_proxy/find_or_create_blob_service.rb4
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb8
-rw-r--r--app/services/dependency_proxy/pull_manifest_service.rb2
-rw-r--r--app/services/deployments/update_environment_service.rb2
-rw-r--r--app/services/design_management/copy_design_collection/copy_service.rb14
-rw-r--r--app/services/draft_notes/publish_service.rb30
-rw-r--r--app/services/environments/auto_stop_service.rb2
-rw-r--r--app/services/environments/stop_service.rb (renamed from app/services/ci/stop_environments_service.rb)23
-rw-r--r--app/services/error_tracking/issue_details_service.rb26
-rw-r--r--app/services/error_tracking/issue_latest_event_service.rb27
-rw-r--r--app/services/error_tracking/issue_update_service.rb32
-rw-r--r--app/services/error_tracking/list_issues_service.rb37
-rw-r--r--app/services/feature_flags/create_service.rb2
-rw-r--r--app/services/feature_flags/destroy_service.rb2
-rw-r--r--app/services/feature_flags/update_service.rb2
-rw-r--r--app/services/git/base_hooks_service.rb5
-rw-r--r--app/services/git/branch_hooks_service.rb7
-rw-r--r--app/services/git/branch_push_service.rb2
-rw-r--r--app/services/git/process_ref_changes_service.rb6
-rw-r--r--app/services/groups/transfer_service.rb4
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb2
-rw-r--r--app/services/issuable/clone/base_service.rb2
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/issuable/destroy_label_links_service.rb2
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/issues/close_service.rb8
-rw-r--r--app/services/issues/create_service.rb5
-rw-r--r--app/services/issues/reopen_service.rb6
-rw-r--r--app/services/jira/requests/base.rb80
-rw-r--r--app/services/members/create_service.rb15
-rw-r--r--app/services/members/import_project_team_service.rb37
-rw-r--r--app/services/merge_requests/base_service.rb4
-rw-r--r--app/services/merge_requests/conflicts/base_service.rb5
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb6
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb6
-rw-r--r--app/services/merge_requests/merge_to_ref_service.rb14
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb2
-rw-r--r--app/services/namespaces/in_product_marketing_emails_service.rb27
-rw-r--r--app/services/notes/create_service.rb8
-rw-r--r--app/services/packages/composer/create_package_service.rb4
-rw-r--r--app/services/packages/create_dependency_service.rb4
-rw-r--r--app/services/packages/debian/generate_distribution_key_service.rb61
-rw-r--r--app/services/packages/debian/generate_distribution_service.rb95
-rw-r--r--app/services/packages/debian/sign_distribution_service.rb38
-rw-r--r--app/services/packages/go/create_package_service.rb2
-rw-r--r--app/services/packages/npm/create_package_service.rb2
-rw-r--r--app/services/packages/nuget/create_dependency_service.rb2
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb63
-rw-r--r--app/services/packages/terraform_module/create_package_service.rb2
-rw-r--r--app/services/packages/update_package_file_service.rb58
-rw-r--r--app/services/packages/update_tags_service.rb2
-rw-r--r--app/services/post_receive_service.rb7
-rw-r--r--app/services/projects/after_rename_service.rb2
-rw-r--r--app/services/projects/cleanup_service.rb2
-rw-r--r--app/services/projects/create_service.rb8
-rw-r--r--app/services/projects/detect_repository_languages_service.rb2
-rw-r--r--app/services/projects/fetch_statistics_increment_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb2
-rw-r--r--app/services/projects/hashed_storage/rollback_repository_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb18
-rw-r--r--app/services/projects/operations/update_service.rb9
-rw-r--r--app/services/projects/overwrite_project_service.rb17
-rw-r--r--app/services/projects/protect_default_branch_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb10
-rw-r--r--app/services/projects/update_pages_service.rb120
-rw-r--r--app/services/projects/update_remote_mirror_service.rb7
-rw-r--r--app/services/releases/base_service.rb9
-rw-r--r--app/services/releases/create_service.rb4
-rw-r--r--app/services/releases/destroy_service.rb2
-rw-r--r--app/services/releases/update_service.rb4
-rw-r--r--app/services/resource_events/change_labels_service.rb2
-rw-r--r--app/services/search/global_service.rb3
-rw-r--r--app/services/security/merge_reports_service.rb70
-rw-r--r--app/services/service_ping/permit_data_categories_service.rb20
-rw-r--r--app/services/service_ping/service_ping_settings.rb17
-rw-r--r--app/services/service_ping/submit_service.rb2
-rw-r--r--app/services/spam/akismet_mark_as_spam_service.rb (renamed from app/services/spam/mark_as_spam_service.rb)6
-rw-r--r--app/services/spam/spam_action_service.rb4
-rw-r--r--app/services/suggestions/create_service.rb2
-rw-r--r--app/services/todos/destroy/destroyed_issuable_service.rb2
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb15
-rw-r--r--app/services/users/ban_service.rb22
-rw-r--r--app/services/users/banned_user_base_service.rb37
-rw-r--r--app/services/users/set_status_service.rb12
-rw-r--r--app/services/users/unban_service.rb15
-rw-r--r--app/uploaders/packages/debian/distribution_release_file_uploader.rb7
-rw-r--r--app/validators/any_field_validator.rb35
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml28
-rw-r--r--app/views/admin/application_settings/_email.html.haml10
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml8
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml12
-rw-r--r--app/views/admin/application_settings/_kroki.html.haml11
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml2
-rw-r--r--app/views/admin/application_settings/_pages.html.haml54
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml4
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml7
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml15
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml3
-rw-r--r--app/views/admin/application_settings/_repository_static_objects.html.haml4
-rw-r--r--app/views/admin/application_settings/_signin.html.haml47
-rw-r--r--app/views/admin/application_settings/_signup.html.haml3
-rw-r--r--app/views/admin/application_settings/_terms.html.haml6
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml1
-rw-r--r--app/views/admin/application_settings/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/general.html.haml6
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml10
-rw-r--r--app/views/admin/application_settings/network.html.haml3
-rw-r--r--app/views/admin/application_settings/preferences.html.haml7
-rw-r--r--app/views/admin/application_settings/repository.html.haml5
-rw-r--r--app/views/admin/applications/_form.html.haml4
-rw-r--r--app/views/admin/applications/new.html.haml7
-rw-r--r--app/views/admin/applications/show.html.haml48
-rw-r--r--app/views/admin/dashboard/index.html.haml5
-rw-r--r--app/views/admin/dev_ops_report/_callout.html.haml13
-rw-r--r--app/views/admin/dev_ops_report/_report.html.haml5
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml1
-rw-r--r--app/views/admin/projects/show.html.haml1
-rw-r--r--app/views/admin/runners/_runner.html.haml80
-rw-r--r--app/views/admin/runners/index.html.haml138
-rw-r--r--app/views/admin/services/_form.html.haml9
-rw-r--r--app/views/admin/services/_service_templates_deprecated_alert.html.haml10
-rw-r--r--app/views/admin/services/edit.html.haml6
-rw-r--r--app/views/admin/services/index.html.haml43
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml3
-rw-r--r--app/views/ci/runner/_how_to_setup_runner_automatically.html.haml21
-rw-r--r--app/views/ci/variables/_index.html.haml1
-rw-r--r--app/views/clusters/clusters/_integrations.html.haml26
-rw-r--r--app/views/clusters/clusters/_multiple_clusters_message.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/devise/shared/_email_opted_in.html.haml4
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml3
-rw-r--r--app/views/groups/_invite_members_modal.html.haml8
-rw-r--r--app/views/groups/dependency_proxies/_url.html.haml6
-rw-r--r--app/views/groups/dependency_proxies/show.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml13
-rw-r--r--app/views/groups/runners/_group_runners.html.haml11
-rw-r--r--app/views/groups/runners/_settings.html.haml3
-rw-r--r--app/views/groups/runners/_sort_dropdown.html.haml (renamed from app/views/admin/runners/_sort_dropdown.html.haml)3
-rw-r--r--app/views/groups/runners/index.html.haml6
-rw-r--r--app/views/groups/settings/_advanced.html.haml4
-rw-r--r--app/views/groups/settings/_lfs.html.haml9
-rw-r--r--app/views/groups/settings/_permissions.html.haml40
-rw-r--r--app/views/groups/settings/_project_access_token_creation.html.haml13
-rw-r--r--app/views/groups/settings/_two_factor_auth.html.haml14
-rw-r--r--app/views/groups/sidebar/_packages.html.haml27
-rw-r--r--app/views/groups/sidebar/_packages_settings.html.haml5
-rw-r--r--app/views/help/instance_configuration.html.haml2
-rw-r--r--app/views/help/instance_configuration/_package_registry.html.haml48
-rw-r--r--app/views/help/instance_configuration/_rate_limit_row.html.haml7
-rw-r--r--app/views/help/instance_configuration/_rate_limits.html.haml36
-rw-r--r--app/views/import/bulk_imports/status.html.haml5
-rw-r--r--app/views/jira_connect/branches/new.html.haml5
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml7
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/_loading_hints.html.haml10
-rw-r--r--app/views/layouts/_mailer.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml3
-rw-r--r--app/views/layouts/_recaptcha_verification.html.haml10
-rw-r--r--app/views/layouts/header/_default.html.haml28
-rw-r--r--app/views/layouts/header/_service_templates_deprecation_callout.html.haml21
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml90
-rw-r--r--app/views/layouts/nav/_explore.html.haml19
-rw-r--r--app/views/layouts/nav/_top_nav_responsive.html.haml2
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml23
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml28
-rw-r--r--app/views/layouts/nav/sidebar/_analytics_links.html.haml22
-rw-r--r--app/views/layouts/nav/sidebar/_group_menus.html.haml163
-rw-r--r--app/views/layouts/nav/sidebar/_wiki_link.html.haml11
-rw-r--r--app/views/notify/member_invited_email.html.haml126
-rw-r--r--app/views/profiles/show.html.haml14
-rw-r--r--app/views/projects/_home_panel.html.haml38
-rw-r--r--app/views/projects/_invite_members_modal.html.haml9
-rw-r--r--app/views/projects/_last_push.html.haml30
-rw-r--r--app/views/projects/_new_project_fields.html.haml3
-rw-r--r--app/views/projects/_terraform_banner.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml16
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/deployments/_confirm_rollback_modal.html.haml23
-rw-r--r--app/views/projects/deployments/_rollback.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/_external_url.html.haml4
-rw-r--r--app/views/projects/environments/_form.html.haml21
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml7
-rw-r--r--app/views/projects/environments/_pin_button.html.haml3
-rw-r--r--app/views/projects/environments/_terminal_button.html.haml3
-rw-r--r--app/views/projects/environments/edit.html.haml7
-rw-r--r--app/views/projects/environments/new.html.haml5
-rw-r--r--app/views/projects/environments/show.html.haml54
-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/issues/_issue.html.haml5
-rw-r--r--app/views/projects/issues/captcha_check.html.haml7
-rw-r--r--app/views/projects/issues/verify.html.haml3
-rw-r--r--app/views/projects/jobs/_table.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml3
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml6
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml4
-rw-r--r--app/views/projects/packages/packages/show.html.haml6
-rw-r--r--app/views/projects/pipelines/show.html.haml4
-rw-r--r--app/views/projects/project_members/index.html.haml43
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml4
-rw-r--r--app/views/projects/security/configuration/show.html.haml3
-rw-r--r--app/views/projects/settings/_general.html.haml2
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/search/results/_blob_data.html.haml15
-rw-r--r--app/views/search/results/_issuable.html.haml2
-rw-r--r--app/views/search/show.html.haml8
-rw-r--r--app/views/shared/_allow_request_access.html.haml8
-rw-r--r--app/views/shared/_captcha_check.html.haml37
-rw-r--r--app/views/shared/_check_recovery_settings.html.haml15
-rw-r--r--app/views/shared/_group_form.html.haml3
-rw-r--r--app/views/shared/_recaptcha_form.html.haml23
-rw-r--r--app/views/shared/_service_ping_consent.html.haml10
-rw-r--r--app/views/shared/access_tokens/_table.html.haml6
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml6
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml23
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml2
-rw-r--r--app/views/shared/doorkeeper/applications/_show.html.haml15
-rw-r--r--app/views/shared/groups/_empty_state.html.haml5
-rw-r--r--app/views/shared/integrations/_form.html.haml3
-rw-r--r--app/views/shared/integrations/_tabs.html.haml18
-rw-r--r--app/views/shared/integrations/edit.html.haml6
-rw-r--r--app/views/shared/integrations/overrides.html.haml10
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_user_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml1
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml3
-rw-r--r--app/views/shared/issuable/form/_type_selector.html.haml6
-rw-r--r--app/views/shared/nav/_sidebar_menu.html.haml49
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_form.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml4
-rw-r--r--app/views/shared/projects/_topics.html.haml32
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml8
-rw-r--r--app/workers/admin_email_worker.rb3
-rw-r--r--app/workers/all_queues.yml59
-rw-r--r--app/workers/analytics/usage_trends/count_job_trigger_worker.rb3
-rw-r--r--app/workers/analytics/usage_trends/counter_job_worker.rb2
-rw-r--r--app/workers/approve_blocked_pending_approval_users_worker.rb2
-rw-r--r--app/workers/authorized_keys_worker.rb2
-rw-r--r--app/workers/authorized_project_update/periodic_recalculate_worker.rb3
-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/project_recalculate_worker.rb4
-rw-r--r--app/workers/authorized_project_update/user_refresh_from_replica_worker.rb1
-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_success_worker.rb2
-rw-r--r--app/workers/bulk_import_worker.rb2
-rw-r--r--app/workers/bulk_imports/entity_worker.rb2
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb2
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb2
-rw-r--r--app/workers/bulk_imports/relation_export_worker.rb2
-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/kill_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/chat_notification_worker.rb2
-rw-r--r--app/workers/ci/archive_trace_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb3
-rw-r--r--app/workers/ci/build_finished_worker.rb3
-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/daily_build_group_report_results_worker.rb2
-rw-r--r--app/workers/ci/delete_objects_worker.rb2
-rw-r--r--app/workers/ci/delete_unit_tests_worker.rb2
-rw-r--r--app/workers/ci/drop_pipeline_worker.rb2
-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.rb2
-rw-r--r--app/workers/ci/pipeline_artifacts/coverage_report_worker.rb2
-rw-r--r--app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb2
-rw-r--r--app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb3
-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/schedule_delete_objects_cron_worker.rb3
-rw-r--r--app/workers/ci/test_failure_history_worker.rb2
-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.rb3
-rw-r--r--app/workers/clusters/applications/deactivate_service_worker.rb10
-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.rb4
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb41
-rw-r--r--app/workers/concerns/gitlab/github_import/queue.rb11
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb35
-rw-r--r--app/workers/container_expiration_policies/cleanup_container_repository_worker.rb15
-rw-r--r--app/workers/container_expiration_policy_worker.rb3
-rw-r--r--app/workers/create_commit_signature_worker.rb2
-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.rb3
-rw-r--r--app/workers/database/drop_detached_partitions_worker.rb18
-rw-r--r--app/workers/database/partition_management_worker.rb3
-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.rb2
-rw-r--r--app/workers/deployments/finished_worker.rb2
-rw-r--r--app/workers/deployments/forward_deployment_worker.rb2
-rw-r--r--app/workers/deployments/hooks_worker.rb2
-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.rb2
-rw-r--r--app/workers/design_management/new_version_worker.rb2
-rw-r--r--app/workers/destroy_pages_deployments_worker.rb2
-rw-r--r--app/workers/detect_repository_languages_worker.rb2
-rw-r--r--app/workers/disallow_two_factor_for_group_worker.rb2
-rw-r--r--app/workers/disallow_two_factor_for_subgroups_worker.rb2
-rw-r--r--app/workers/email_receiver_worker.rb5
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/environments/auto_delete_cron_worker.rb34
-rw-r--r--app/workers/environments/auto_stop_cron_worker.rb4
-rw-r--r--app/workers/environments/canary_ingress/update_worker.rb2
-rw-r--r--app/workers/error_tracking_issue_link_worker.rb2
-rw-r--r--app/workers/experiments/record_conversion_event_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb3
-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/export_csv_worker.rb2
-rw-r--r--app/workers/file_hook_worker.rb2
-rw-r--r--app/workers/flush_counter_increments_worker.rb2
-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.rb1
-rw-r--r--app/workers/gitlab/github_import/import_pull_request_review_worker.rb1
-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.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb2
-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.rb8
-rw-r--r--app/workers/gitlab/import/stuck_import_job.rb26
-rw-r--r--app/workers/gitlab/jira_import/advance_stage_worker.rb2
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb6
-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_service_ping_worker.rb3
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb2
-rw-r--r--app/workers/group_export_worker.rb2
-rw-r--r--app/workers/group_import_worker.rb2
-rw-r--r--app/workers/hashed_storage/migrator_worker.rb7
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb7
-rw-r--r--app/workers/hashed_storage/project_rollback_worker.rb7
-rw-r--r--app/workers/hashed_storage/rollbacker_worker.rb7
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb3
-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_v2.rb3
-rw-r--r--app/workers/invalid_gpg_signature_update_worker.rb2
-rw-r--r--app/workers/irker_worker.rb3
-rw-r--r--app/workers/issuable/label_links_destroy_worker.rb2
-rw-r--r--app/workers/issuable_export_csv_worker.rb2
-rw-r--r--app/workers/issuables/clear_groups_issue_counter_worker.rb2
-rw-r--r--app/workers/issue_due_scheduler_worker.rb3
-rw-r--r--app/workers/issue_placement_worker.rb2
-rw-r--r--app/workers/issue_rebalancing_worker.rb2
-rw-r--r--app/workers/jira_connect/forward_event_worker.rb16
-rw-r--r--app/workers/jira_connect/retry_request_worker.rb22
-rw-r--r--app/workers/jira_connect/sync_branch_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_builds_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_deployments_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_feature_flags_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_merge_request_worker.rb2
-rw-r--r--app/workers/jira_connect/sync_project_worker.rb2
-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.rb2
-rw-r--r--app/workers/merge_request_mergeability_check_worker.rb10
-rw-r--r--app/workers/merge_requests/create_pipeline_worker.rb2
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb2
-rw-r--r--app/workers/merge_requests/handle_assignees_change_worker.rb2
-rw-r--r--app/workers/merge_requests/resolve_todos_worker.rb2
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/metrics/dashboard/prune_old_annotations_worker.rb2
-rw-r--r--app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb3
-rw-r--r--app/workers/metrics/dashboard/sync_dashboards_worker.rb2
-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.rb3
-rw-r--r--app/workers/namespaces/onboarding_issue_created_worker.rb2
-rw-r--r--app/workers/namespaces/onboarding_pipeline_created_worker.rb2
-rw-r--r--app/workers/namespaces/onboarding_progress_worker.rb3
-rw-r--r--app/workers/namespaces/onboarding_user_added_worker.rb2
-rw-r--r--app/workers/namespaces/prune_aggregation_schedules_worker.rb3
-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.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_pool/create_worker.rb2
-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/background_move_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb2
-rw-r--r--app/workers/packages/composer/cache_cleanup_worker.rb3
-rw-r--r--app/workers/packages/composer/cache_update_worker.rb2
-rw-r--r--app/workers/packages/debian/generate_distribution_worker.rb2
-rw-r--r--app/workers/packages/debian/process_changes_worker.rb2
-rw-r--r--app/workers/packages/go/sync_packages_worker.rb2
-rw-r--r--app/workers/packages/helm/extraction_worker.rb2
-rw-r--r--app/workers/packages/maven/metadata/sync_worker.rb2
-rw-r--r--app/workers/packages/nuget/extraction_worker.rb2
-rw-r--r--app/workers/packages/rubygems/extraction_worker.rb2
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb3
-rw-r--r--app/workers/pages_domain_ssl_renewal_cron_worker.rb4
-rw-r--r--app/workers/pages_domain_ssl_renewal_worker.rb2
-rw-r--r--app/workers/pages_domain_verification_cron_worker.rb4
-rw-r--r--app/workers/pages_domain_verification_worker.rb2
-rw-r--r--app/workers/pages_remove_worker.rb2
-rw-r--r--app/workers/pages_transfer_worker.rb2
-rw-r--r--app/workers/pages_update_configuration_worker.rb2
-rw-r--r--app/workers/pages_worker.rb3
-rw-r--r--app/workers/partition_creation_worker.rb3
-rw-r--r--app/workers/personal_access_tokens/expired_notification_worker.rb3
-rw-r--r--app/workers/personal_access_tokens/expiring_worker.rb3
-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.rb1
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb3
-rw-r--r--app/workers/post_receive.rb3
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--app/workers/project_cache_worker.rb2
-rw-r--r--app/workers/project_daily_statistics_worker.rb2
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--app/workers/project_export_worker.rb2
-rw-r--r--app/workers/project_service_worker.rb4
-rw-r--r--app/workers/projects/post_creation_worker.rb2
-rw-r--r--app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb2
-rw-r--r--app/workers/prometheus/create_default_alerts_worker.rb2
-rw-r--r--app/workers/propagate_integration_group_worker.rb4
-rw-r--r--app/workers/propagate_integration_inherit_descendant_worker.rb4
-rw-r--r--app/workers/propagate_integration_inherit_worker.rb4
-rw-r--r--app/workers/propagate_integration_project_worker.rb4
-rw-r--r--app/workers/propagate_integration_worker.rb6
-rw-r--r--app/workers/propagate_service_template_worker.rb5
-rw-r--r--app/workers/prune_old_events_worker.rb3
-rw-r--r--app/workers/purge_dependency_proxy_cache_worker.rb2
-rw-r--r--app/workers/rebase_worker.rb2
-rw-r--r--app/workers/releases/create_evidence_worker.rb2
-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.rb3
-rw-r--r--app/workers/remove_expired_members_worker.rb3
-rw-r--r--app/workers/remove_unaccepted_member_invites_worker.rb3
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb3
-rw-r--r--app/workers/repository_archive_cache_worker.rb3
-rw-r--r--app/workers/repository_check/batch_worker.rb2
-rw-r--r--app/workers/repository_check/clear_worker.rb2
-rw-r--r--app/workers/repository_check/dispatch_worker.rb3
-rw-r--r--app/workers/repository_check/single_repository_worker.rb2
-rw-r--r--app/workers/repository_cleanup_worker.rb2
-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.rb25
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb2
-rw-r--r--app/workers/requests_profiles_worker.rb3
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/schedule_merge_request_cleanup_refs_worker.rb3
-rw-r--r--app/workers/schedule_migrate_external_diffs_worker.rb3
-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.rb5
-rw-r--r--app/workers/snippets/schedule_bulk_repository_shard_moves_worker.rb2
-rw-r--r--app/workers/ssh_keys/expired_notification_worker.rb3
-rw-r--r--app/workers/ssh_keys/expiring_soon_notification_worker.rb3
-rw-r--r--app/workers/stage_update_worker.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb3
-rw-r--r--app/workers/stuck_export_jobs_worker.rb3
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb3
-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.rb2
-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.rb3
-rw-r--r--app/workers/update_container_registry_info_worker.rb3
-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.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
-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.rb3
-rw-r--r--app/workers/users/deactivate_dormant_users_worker.rb2
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb2
-rw-r--r--app/workers/web_hook_worker.rb10
-rw-r--r--app/workers/web_hooks/destroy_worker.rb3
-rw-r--r--app/workers/web_hooks/log_execution_worker.rb4
-rw-r--r--app/workers/x509_certificate_revoke_worker.rb2
-rw-r--r--app/workers/x509_issuer_crl_check_worker.rb3
1821 files changed, 19563 insertions, 11742 deletions
diff --git a/app/assets/images/cluster_app_logos/cert_manager.png b/app/assets/images/cluster_app_logos/cert_manager.png
deleted file mode 100644
index bbc867858da..00000000000
--- a/app/assets/images/cluster_app_logos/cert_manager.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/crossplane.png b/app/assets/images/cluster_app_logos/crossplane.png
deleted file mode 100644
index 32d8175108c..00000000000
--- a/app/assets/images/cluster_app_logos/crossplane.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/elastic_stack.png b/app/assets/images/cluster_app_logos/elastic_stack.png
deleted file mode 100644
index 69fbc6aacd0..00000000000
--- a/app/assets/images/cluster_app_logos/elastic_stack.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/elasticsearch.png b/app/assets/images/cluster_app_logos/elasticsearch.png
deleted file mode 100644
index 96e9e0ff934..00000000000
--- a/app/assets/images/cluster_app_logos/elasticsearch.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/gitlab.png b/app/assets/images/cluster_app_logos/gitlab.png
deleted file mode 100644
index cb2195fc6a2..00000000000
--- a/app/assets/images/cluster_app_logos/gitlab.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/helm.png b/app/assets/images/cluster_app_logos/helm.png
deleted file mode 100644
index 2989cae7b93..00000000000
--- a/app/assets/images/cluster_app_logos/helm.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/jeager.png b/app/assets/images/cluster_app_logos/jeager.png
deleted file mode 100644
index be5bf2a0c9c..00000000000
--- a/app/assets/images/cluster_app_logos/jeager.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/jupyterhub.png b/app/assets/images/cluster_app_logos/jupyterhub.png
deleted file mode 100644
index 80c7343067f..00000000000
--- a/app/assets/images/cluster_app_logos/jupyterhub.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png
deleted file mode 100644
index 0a2510c8549..00000000000
--- a/app/assets/images/cluster_app_logos/knative.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/kubernetes.png b/app/assets/images/cluster_app_logos/kubernetes.png
deleted file mode 100644
index 4d774909c10..00000000000
--- a/app/assets/images/cluster_app_logos/kubernetes.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/meltano.png b/app/assets/images/cluster_app_logos/meltano.png
deleted file mode 100644
index 7a2d82fbe27..00000000000
--- a/app/assets/images/cluster_app_logos/meltano.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/modsecurity.png b/app/assets/images/cluster_app_logos/modsecurity.png
deleted file mode 100644
index fd58275e1d7..00000000000
--- a/app/assets/images/cluster_app_logos/modsecurity.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/prometheus.png b/app/assets/images/cluster_app_logos/prometheus.png
deleted file mode 100644
index a8663449b88..00000000000
--- a/app/assets/images/cluster_app_logos/prometheus.png
+++ /dev/null
Binary files differ
diff --git a/app/views/shared/icons/_dev_ops_report_overview.svg b/app/assets/images/dev_ops_report_overview.svg
index 2f31113bad7..2f31113bad7 100644
--- a/app/views/shared/icons/_dev_ops_report_overview.svg
+++ b/app/assets/images/dev_ops_report_overview.svg
diff --git a/app/assets/images/gitorious-logo-black.png b/app/assets/images/gitorious-logo-black.png
deleted file mode 100644
index 4a55fdc225a..00000000000
--- a/app/assets/images/gitorious-logo-black.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/gitorious-logo-blue.png b/app/assets/images/gitorious-logo-blue.png
deleted file mode 100644
index 5eaa327d3df..00000000000
--- a/app/assets/images/gitorious-logo-blue.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/admin_verify-0.png b/app/assets/images/mailers/in_product_marketing/admin_verify-0.png
new file mode 100644
index 00000000000..c6d3e55afc1
--- /dev/null
+++ b/app/assets/images/mailers/in_product_marketing/admin_verify-0.png
Binary files differ
diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue
index 4e9cefbfdd7..e5ab0f9123f 100644
--- a/app/assets/javascripts/admin/users/components/actions/ban.vue
+++ b/app/assets/javascripts/admin/users/components/actions/ban.vue
@@ -6,10 +6,11 @@ import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
const messageHtml = `
- <p>${s__('AdminUsers|When banned, users:')}</p>
+ <p>${s__('AdminUsers|When banned:')}</p>
<ul>
- <li>${s__("AdminUsers|Can't log in.")}</li>
- <li>${s__("AdminUsers|Can't access Git repositories.")}</li>
+ <li>${s__("AdminUsers|The user can't log in.")}</li>
+ <li>${s__("AdminUsers|The user can't access git repositories.")}</li>
+ <li>${s__('AdminUsers|Issues authored by this user are hidden from other users.')}</li>
</ul>
<p>${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}</p>
<p>${sprintf(
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
index 6f4f272154a..a0f4a4bf382 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -28,6 +28,7 @@ export default {
modal-type="delete"
:username="username"
:paths="paths"
+ :delete-path="paths.delete"
:oncall-schedules="oncallSchedules"
>
<slot></slot>
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 82b09c04ab2..02fd3efafa1 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
@@ -28,6 +28,7 @@ export default {
modal-type="delete-with-contributions"
:username="username"
:paths="paths"
+ :delete-path="paths.deleteWithContributions"
:oncall-schedules="oncallSchedules"
>
<slot></slot>
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 b3b68442e80..a1589c9d46d 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
@@ -14,6 +14,10 @@ export default {
type: Object,
required: true,
},
+ deletePath: {
+ type: String,
+ required: true,
+ },
modalType: {
type: String,
required: true,
@@ -27,7 +31,7 @@ export default {
modalAttributes() {
return {
'data-block-user-url': this.paths.block,
- 'data-delete-user-url': this.paths.delete,
+ 'data-delete-user-url': this.deletePath,
'data-gl-modal-action': this.modalType,
'data-username': this.username,
'data-oncall-schedules': JSON.stringify(this.oncallSchedules),
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
index 1a3289ffb75..238081cc3c0 100644
--- a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score.vue
@@ -1,7 +1,9 @@
<script>
import { GlBadge, GlTable, GlLink, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { sprintf, s__ } from '~/locale';
+import DevopsScoreCallout from './devops_score_callout.vue';
const defaultHeaderAttrs = {
thClass: 'gl-bg-white!',
@@ -15,14 +17,12 @@ export default {
GlSingleStat,
GlLink,
GlEmptyState,
+ DevopsScoreCallout,
},
inject: {
devopsScoreMetrics: {
default: null,
},
- devopsReportDocsPath: {
- default: '',
- },
noDataImagePath: {
default: '',
},
@@ -40,6 +40,7 @@ export default {
return this.devopsScoreMetrics.averageScore === undefined;
},
},
+ devopsReportDocsPath: helpPagePath('user/admin_area/analytics/dev_ops_report'),
tableHeaderFields: [
{
key: 'title',
@@ -65,46 +66,49 @@ export default {
};
</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"
+ <div data-testid="devops-score-container">
+ <devops-score-callout />
+ <gl-empty-state
+ v-if="isEmpty"
+ :title="__('Data is still calculating...')"
+ :svg-path="noDataImagePath"
>
- <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 #description>
+ <p class="gl-mb-0">{{ __('It may be several days before you see feature usage data.') }}</p>
+ <gl-link :href="$options.devopsReportDocsPath">{{
+ __('See example DevOps Score page in our documentation.')
+ }}</gl-link>
</template>
- </gl-table>
+ </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>
</div>
</template>
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue b/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
new file mode 100644
index 00000000000..e594b4e360a
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { parseBoolean, getCookie, setCookie } from '~/lib/utils/common_utils';
+import {
+ INTRO_COOKIE_KEY,
+ INTRO_BANNER_TITLE,
+ INTRO_BANNER_BODY,
+ INTRO_BANNER_ACTION_TEXT,
+} from '../constants';
+
+export default {
+ name: 'DevopsScoreCallout',
+ components: {
+ GlBanner,
+ },
+ inject: {
+ devopsReportDocsPath: {
+ default: '',
+ },
+ devopsScoreIntroImagePath: {
+ default: '',
+ },
+ },
+ data() {
+ return {
+ bannerDismissed: parseBoolean(getCookie(INTRO_COOKIE_KEY)),
+ };
+ },
+ i18n: {
+ title: INTRO_BANNER_TITLE,
+ body: INTRO_BANNER_BODY,
+ action: INTRO_BANNER_ACTION_TEXT,
+ },
+ methods: {
+ dismissBanner() {
+ setCookie(INTRO_COOKIE_KEY, 'true');
+ this.bannerDismissed = true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-banner
+ v-if="!bannerDismissed"
+ class="gl-mt-3"
+ variant="introduction"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.action"
+ :button-link="devopsReportDocsPath"
+ :svg-path="devopsScoreIntroImagePath"
+ @close="dismissBanner"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
index 7c14cf3767f..400326e41e1 100644
--- a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
@@ -1,5 +1,6 @@
<script>
import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: {
@@ -15,13 +16,11 @@ export default {
svgPath: {
default: '',
},
- docsLink: {
- default: '',
- },
primaryButtonPath: {
default: '',
},
},
+ docsLink: helpPagePath('development/service_ping/index.md'),
};
</script>
<template>
@@ -36,7 +35,7 @@ export default {
"
>
<template #docLink="{ content }">
- <gl-link :href="docsLink" target="_blank" data-testid="docs-link">{{ content }}</gl-link>
+ <gl-link :href="$options.docsLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else>
@@ -44,12 +43,7 @@ export default {
{{ s__('ServicePing|Turn on service ping to review instance-level analytics.') }}
</p>
- <gl-button
- category="primary"
- variant="success"
- :href="primaryButtonPath"
- data-testid="power-on-button"
- >
+ <gl-button category="primary" variant="success" :href="primaryButtonPath">
{{ s__('ServicePing|Turn on service ping') }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/analytics/devops_report/constants.js b/app/assets/javascripts/analytics/devops_report/constants.js
new file mode 100644
index 00000000000..b395d7eb464
--- /dev/null
+++ b/app/assets/javascripts/analytics/devops_report/constants.js
@@ -0,0 +1,11 @@
+import { __ } from '~/locale';
+
+export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
+
+export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
+
+export const INTRO_BANNER_BODY = __(
+ 'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
+);
+
+export const INTRO_BANNER_ACTION_TEXT = __('Read more');
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_report/devops_score.js
index 18f7cf0c3ab..0bf98b65ed5 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score.js
@@ -6,14 +6,14 @@ export default () => {
if (!el) return false;
- const { devopsScoreMetrics, devopsReportDocsPath, noDataImagePath } = el.dataset;
+ const { devopsScoreMetrics, noDataImagePath, devopsScoreIntroImagePath } = el.dataset;
return new Vue({
el,
provide: {
devopsScoreMetrics: JSON.parse(devopsScoreMetrics),
- devopsReportDocsPath,
noDataImagePath,
+ devopsScoreIntroImagePath,
},
render(h) {
return h(DevopsScore);
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
index 63b36f35247..eb2992422a4 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
@@ -11,12 +11,7 @@ export default () => {
if (!emptyStateContainer) return false;
- const {
- isAdmin,
- emptyStateSvgPath,
- enableServicePingPath,
- docsLink,
- } = emptyStateContainer.dataset;
+ const { isAdmin, emptyStateSvgPath, enableServicePingPath } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
@@ -24,7 +19,6 @@ export default () => {
isAdmin: parseBoolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableServicePingPath,
- docsLink,
},
render(h) {
return h(ServicePingDisabled);
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js
index fd9b0160b0d..c7a53288ae4 100644
--- a/app/assets/javascripts/api/analytics_api.js
+++ b/app/assets/javascripts/api/analytics_api.js
@@ -1,51 +1,83 @@
import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
import { buildApiUrl } from './api_utils';
-const GROUP_VSA_PATH_BASE =
- '/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
-const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
+const PROJECT_VSA_METRICS_BASE = '/:request_path/-/analytics/value_stream_analytics';
+const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
+const PROJECT_VSA_STAGE_DATA_PATH = `${PROJECT_VSA_STAGES_PATH}/:stage_id`;
-const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
+export const METRIC_TYPE_SUMMARY = 'summary';
+export const METRIC_TYPE_TIME_SUMMARY = 'time_summary';
+
+const buildProjectMetricsPath = (requestPath) =>
+ buildApiUrl(PROJECT_VSA_METRICS_BASE).replace(':request_path', requestPath);
+
+const buildProjectValueStreamPath = (requestPath, valueStreamId = null) => {
if (valueStreamId) {
return buildApiUrl(PROJECT_VSA_STAGES_PATH)
- .replace(':project_path', projectPath)
+ .replace(':request_path', requestPath)
.replace(':value_stream_id', valueStreamId);
}
- return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
+ return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':request_path', requestPath);
};
-const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) =>
- buildApiUrl(GROUP_VSA_PATH_BASE)
- .replace(':id', groupId)
+const buildValueStreamStageDataPath = ({ requestPath, valueStreamId = null, stageId = null }) =>
+ buildApiUrl(PROJECT_VSA_STAGE_DATA_PATH)
+ .replace(':request_path', requestPath)
.replace(':value_stream_id', valueStreamId)
.replace(':stage_id', stageId);
-export const getProjectValueStreams = (projectPath) => {
- const url = buildProjectValueStreamPath(projectPath);
+export const getProjectValueStreams = (requestPath) => {
+ const url = buildProjectValueStreamPath(requestPath);
return axios.get(url);
};
-export const getProjectValueStreamStages = (projectPath, valueStreamId) => {
- const url = buildProjectValueStreamPath(projectPath, valueStreamId);
+export const getProjectValueStreamStages = (requestPath, valueStreamId) => {
+ const url = buildProjectValueStreamPath(requestPath, valueStreamId);
return axios.get(url);
};
// NOTE: legacy VSA request use a different path
// the `requestPath` provides a full url for the request
export const getProjectValueStreamStageData = ({ requestPath, stageId, params }) =>
- axios.get(`${requestPath}/events/${stageId}`, { params });
+ axios.get(joinPaths(requestPath, 'events', stageId), { params });
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
/**
- * Shared group VSA paths
- * We share some endpoints across and group and project level VSA
- * When used for project level VSA, requests should include the `project_id` in the params object
+ * Dedicated project VSA paths
*/
-export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => {
- const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId });
- return axios.get(`${stageBase}/median`, { params });
+export const getValueStreamStageMedian = ({ requestPath, valueStreamId, stageId }, params = {}) => {
+ const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
+ return axios.get(joinPaths(stageBase, 'median'), { params });
+};
+
+export const getValueStreamStageRecords = (
+ { requestPath, valueStreamId, stageId },
+ params = {},
+) => {
+ const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
+ return axios.get(joinPaths(stageBase, 'records'), { params });
+};
+
+export const getValueStreamStageCounts = ({ requestPath, valueStreamId, stageId }, params = {}) => {
+ const stageBase = buildValueStreamStageDataPath({ requestPath, valueStreamId, stageId });
+ return axios.get(joinPaths(stageBase, 'count'), { params });
+};
+
+export const getValueStreamMetrics = ({
+ endpoint = METRIC_TYPE_SUMMARY,
+ requestPath,
+ params = {},
+}) => {
+ const metricBase = buildProjectMetricsPath(requestPath);
+ return axios.get(joinPaths(metricBase, endpoint), { params });
+};
+
+export const getValueStreamSummaryMetrics = (requestPath, params = {}) => {
+ const metricBase = buildProjectMetricsPath(requestPath);
+ return axios.get(joinPaths(metricBase, 'summary'), { params });
};
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 7934eac2f7e..4698fcd4d42 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -11,7 +11,7 @@ import renderMetrics from './render_metrics';
// Delegates to syntax highlight and render math & mermaid diagrams.
//
$.fn.renderGFM = function renderGFM() {
- syntaxHighlight(this.find('.js-syntax-highlight'));
+ syntaxHighlight(this.find('.js-syntax-highlight').get());
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 9c023235428..4742b4ae4b4 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ isBinary: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
activeViewerType: {
type: String,
required: false,
@@ -81,6 +86,7 @@ export default {
:raw-path="blob.rawPath"
:active-viewer="viewer"
:has-render-error="hasRenderError"
+ :is-binary="isBinary"
@copy="proxyCopyRequest"
/>
</div>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index b9f2c5b42e4..2798a918b15 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ isBinary: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
downloadUrl() {
@@ -43,6 +48,9 @@ export default {
getBlobHashTarget() {
return `[data-blob-hash="${this.blobHash}"]`;
},
+ showCopyButton() {
+ return !this.hasRenderError && !this.isBinary;
+ },
},
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
@@ -52,7 +60,7 @@ export default {
<template>
<gl-button-group data-qa-selector="default_actions_container">
<gl-button
- v-if="!hasRenderError"
+ v-if="showCopyButton"
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
@@ -65,6 +73,7 @@ export default {
variant="default"
/>
<gl-button
+ v-if="!isBinary"
v-gl-tooltip.hover
:aria-label="$options.BTN_RAW_TITLE"
:title="$options.BTN_RAW_TITLE"
diff --git a/app/assets/javascripts/blob/csv/csv_viewer.vue b/app/assets/javascripts/blob/csv/csv_viewer.vue
index 050f2785d9a..1f9d20a487f 100644
--- a/app/assets/javascripts/blob/csv/csv_viewer.vue
+++ b/app/assets/javascripts/blob/csv/csv_viewer.vue
@@ -1,11 +1,12 @@
<script>
-import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import Papa from 'papaparse';
+import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
export default {
components: {
+ PapaParseAlert,
GlTable,
- GlAlert,
GlLoadingIcon,
},
props: {
@@ -17,7 +18,7 @@ export default {
data() {
return {
items: [],
- errorMessage: null,
+ papaParseErrors: [],
loading: true,
};
},
@@ -26,7 +27,7 @@ export default {
this.items = parsed.data;
if (parsed.errors.length) {
- this.errorMessage = parsed.errors.map((e) => e.message).join('. ');
+ this.papaParseErrors = parsed.errors;
}
this.loading = false;
@@ -40,9 +41,7 @@ export default {
<gl-loading-icon class="gl-mt-5" size="lg" />
</div>
<div v-else>
- <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
- {{ errorMessage }}
- </gl-alert>
+ <papa-parse-alert v-if="papaParseErrors.length" :papa-parse-errors="papaParseErrors" />
<gl-table
:empty-text="__('No CSV data to display.')"
:items="items"
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 4d133659daa..1bda7d4e3f0 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -36,6 +36,34 @@ const loadRichBlobViewer = (type) => {
}
};
+const loadViewer = (viewerParam) => {
+ const viewer = viewerParam;
+ const url = viewer.getAttribute('data-url');
+
+ if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+ return Promise.resolve(viewer);
+ }
+
+ viewer.setAttribute('data-loading', 'true');
+
+ return axios.get(url).then(({ data }) => {
+ viewer.innerHTML = data.html;
+
+ window.requestIdleCallback(() => {
+ viewer.removeAttribute('data-loading');
+ });
+
+ return viewer;
+ });
+};
+
+export const initAuxiliaryViewer = () => {
+ const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
+ if (!auxiliaryViewer) return;
+
+ loadViewer(auxiliaryViewer);
+};
+
export const handleBlobRichViewer = (viewer, type) => {
if (!viewer || !type) return;
@@ -49,27 +77,20 @@ export const handleBlobRichViewer = (viewer, type) => {
});
};
-export default class BlobViewer {
+export class BlobViewer {
constructor() {
performanceMarkAndMeasure({
mark: REPO_BLOB_LOAD_VIEWER_START,
});
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
const type = viewer?.dataset?.richType;
- BlobViewer.initAuxiliaryViewer();
+ initAuxiliaryViewer();
handleBlobRichViewer(viewer, type);
this.initMainViewers();
}
- static initAuxiliaryViewer() {
- const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
- if (!auxiliaryViewer) return;
-
- BlobViewer.loadViewer(auxiliaryViewer);
- }
-
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
@@ -173,7 +194,7 @@ export default class BlobViewer {
this.activeViewer = newViewer;
this.toggleCopyButtonState();
- BlobViewer.loadViewer(newViewer)
+ loadViewer(newViewer)
.then((viewer) => {
$(viewer).renderGFM();
window.requestIdleCallback(() => {
@@ -205,25 +226,4 @@ export default class BlobViewer {
}),
);
}
-
- static loadViewer(viewerParam) {
- const viewer = viewerParam;
- const url = viewer.getAttribute('data-url');
-
- if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
- return Promise.resolve(viewer);
- }
-
- viewer.setAttribute('data-loading', 'true');
-
- return axios.get(url).then(({ data }) => {
- viewer.innerHTML = data.html;
-
- window.requestIdleCallback(() => {
- viewer.removeAttribute('data-loading');
- });
-
- return viewer;
- });
- }
}
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 7bfda46d71c..e068910c626 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import SourceEditor from '~/editor/source_editor';
+import { getBlobLanguage } from '~/editor/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
@@ -16,16 +17,7 @@ export default class EditBlob {
this.configureMonacoEditor();
if (this.options.isMarkdown) {
- import('~/editor/extensions/source_editor_markdown_ext')
- .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
- this.editor.use(new MarkdownExtension());
- addEditorMarkdownListeners(this.editor);
- })
- .catch((e) =>
- createFlash({
- message: `${BLOB_EDITOR_ERROR}: ${e}`,
- }),
- );
+ this.fetchMarkdownExtension();
}
this.initModePanesAndLinks();
@@ -34,12 +26,30 @@ export default class EditBlob {
this.editor.focus();
}
+ fetchMarkdownExtension() {
+ import('~/editor/extensions/source_editor_markdown_ext')
+ .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
+ this.editor.use(
+ new MarkdownExtension({ instance: this.editor, projectPath: this.options.projectPath }),
+ );
+ this.hasMarkdownExtension = true;
+ addEditorMarkdownListeners(this.editor);
+ })
+ .catch((e) =>
+ createFlash({
+ message: `${BLOB_EDITOR_ERROR}: ${e}`,
+ }),
+ );
+ }
+
configureMonacoEditor() {
const editorEl = document.getElementById('editor');
const fileNameEl = document.getElementById('file_path') || document.getElementById('file_name');
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
+ this.hasMarkdownExtension = false;
+
const rootEditor = new SourceEditor();
this.editor = rootEditor.createInstance({
@@ -51,6 +61,12 @@ export default class EditBlob {
fileNameEl.addEventListener('change', () => {
this.editor.updateModelLanguage(fileNameEl.value);
+ const newLang = getBlobLanguage(fileNameEl.value);
+ if (newLang === 'markdown') {
+ if (!this.hasMarkdownExtension) {
+ this.fetchMarkdownExtension();
+ }
+ }
});
form.addEventListener('submit', () => {
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 46f97e09385..3219d74f85f 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -204,6 +204,9 @@ export const FiltersInfo = {
releaseTag: {
negatedSupport: true,
},
+ types: {
+ negatedSupport: true,
+ },
search: {
negatedSupport: false,
},
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 05b64ddc773..5658a34e9a6 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -65,7 +65,7 @@ export default {
},
computed: {
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
- ...mapGetters(['isEpicBoard']),
+ ...mapGetters(['isEpicBoard', 'isProjectBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
@@ -144,6 +144,9 @@ export default {
totalProgress() {
return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
},
+ showReferencePath() {
+ return !this.isProjectBoard && this.itemReferencePath;
+ },
},
methods: {
...mapActions(['performSearch', 'setError']),
@@ -247,7 +250,7 @@ export default {
:class="{ 'gl-font-base': isEpicBoard }"
>
<tooltip-on-truncate
- v-if="itemReferencePath"
+ v-if="showReferencePath"
:title="itemReferencePath"
placement="bottom"
class="board-item-path gl-text-truncate gl-font-weight-bold"
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 69abf886ad7..bcf5b12b209 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -79,7 +79,7 @@ export default {
'is-collapsed': list.collapsed,
'board-type-assignee': list.listType === 'assignee',
}"
- :data-id="list.id"
+ :data-list-id="list.id"
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable"
data-qa-selector="board_list"
>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 53b071aaed1..4df6ff75249 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -6,10 +6,12 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { DraggableItemTypes } from '../constants';
import BoardColumn from './board_column.vue';
import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
+ draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
BoardColumn,
@@ -76,19 +78,6 @@ export default {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
- handleDragOnEnd(params) {
- const { item, newIndex, oldIndex, to } = params;
-
- const listId = item.dataset.id;
- const replacedListId = to.children[newIndex].dataset.id;
-
- this.moveList({
- listId,
- replacedListId,
- newIndex,
- adjustmentValue: newIndex < oldIndex ? 1 : -1,
- });
- },
},
};
</script>
@@ -104,7 +93,7 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
- @end="handleDragOnEnd"
+ @end="moveList"
>
<component
:is="boardColumnComponent"
@@ -112,6 +101,7 @@ export default {
:key="index"
ref="board"
:list="list"
+ :data-draggable-item-type="$options.draggableItemTypes.list"
:disabled="disabled"
/>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index e014b82d362..7a936e75676 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -87,6 +87,7 @@ export default {
v-bind="$attrs"
:open="isSidebarOpen"
class="boards-sidebar gl-absolute"
+ variant="sidebar"
@close="handleClose"
>
<template #title>
@@ -159,7 +160,7 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
- <board-sidebar-labels-select class="labels" />
+ <board-sidebar-labels-select class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index cfd6b21fa66..7f242dea644 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -27,7 +27,15 @@ export default {
},
computed: {
urlParams() {
- const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ } = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
@@ -36,6 +44,9 @@ export default {
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
'not[assignee_username]': this.filterParams.not.assigneeUsername,
+ 'not[types]': this.filterParams.not.types,
+ 'not[milestone_title]': this.filterParams.not.milestoneTitle,
+ 'not[weight]': this.filterParams.not.weight,
},
undefined,
);
@@ -46,7 +57,10 @@ export default {
author_username: authorUsername,
'label_name[]': labelName,
assignee_username: assigneeUsername,
+ milestone_title: milestoneTitle,
search,
+ types,
+ weight,
};
},
},
@@ -64,7 +78,15 @@ export default {
this.performSearch();
},
getFilteredSearchValue() {
- const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
+ const {
+ authorUsername,
+ labelName,
+ assigneeUsername,
+ search,
+ milestoneTitle,
+ types,
+ weight,
+ } = this.filterParams;
const filteredSearchValue = [];
if (authorUsername) {
@@ -81,6 +103,13 @@ export default {
});
}
+ if (types) {
+ filteredSearchValue.push({
+ type: 'types',
+ value: { data: types, operator: '=' },
+ });
+ }
+
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
@@ -90,6 +119,20 @@ export default {
);
}
+ if (milestoneTitle) {
+ filteredSearchValue.push({
+ type: 'milestone_title',
+ value: { data: milestoneTitle, operator: '=' },
+ });
+ }
+
+ if (weight) {
+ filteredSearchValue.push({
+ type: 'weight',
+ value: { data: weight, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author_username',
@@ -97,6 +140,20 @@ export default {
});
}
+ if (this.filterParams['not[milestoneTitle]']) {
+ filteredSearchValue.push({
+ type: 'milestone_title',
+ value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
+ });
+ }
+
+ if (this.filterParams['not[weight]']) {
+ filteredSearchValue.push({
+ type: 'weight',
+ value: { data: this.filterParams['not[weight]'], operator: '!=' },
+ });
+ }
+
if (this.filterParams['not[assigneeUsername]']) {
filteredSearchValue.push({
type: 'assignee_username',
@@ -113,6 +170,13 @@ export default {
);
}
+ if (this.filterParams['not[types]']) {
+ filteredSearchValue.push({
+ type: 'types',
+ value: { data: this.filterParams['not[types]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
@@ -140,9 +204,18 @@ export default {
case 'assignee_username':
filterParams.assigneeUsername = filter.value.data;
break;
+ case 'types':
+ filterParams.types = filter.value.data;
+ break;
case 'label_name':
labels.push(filter.value.data);
break;
+ case 'milestone_title':
+ filterParams.milestoneTitle = filter.value.data;
+ break;
+ case 'weight':
+ filterParams.weight = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 386ed6bd0a1..a89f71504a9 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,7 +2,7 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
-import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
+import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -18,10 +18,9 @@ const boardDefaults = {
id: false,
name: '',
labels: [],
- milestone_id: undefined,
+ milestone: {},
iteration_id: undefined,
assignee: {},
- assignee_id: undefined,
weight: null,
hide_backlog_list: false,
hide_closed_list: false,
@@ -190,13 +189,10 @@ export default {
issueBoardScopeMutationVariables() {
return {
weight: this.board.weight,
- assigneeId: this.board.assignee?.id
- ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
+ assigneeId: this.board.assignee?.id || null,
+ milestoneId: this.board.milestone?.id
+ ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
- milestoneId:
- this.board.milestone?.id || this.board.milestone?.id === 0
- ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
- : null,
iterationId: this.board.iteration_id
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
@@ -306,6 +302,19 @@ export default {
}
});
},
+ setAssignee(assigneeId) {
+ this.$set(this.board, 'assignee', {
+ id: assigneeId,
+ });
+ },
+ setMilestone(milestoneId) {
+ this.$set(this.board, 'milestone', {
+ id: milestoneId,
+ });
+ },
+ setWeight(weight) {
+ this.$set(this.board, 'weight', weight);
+ },
},
};
</script>
@@ -373,6 +382,9 @@ export default {
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
+ @set-assignee="setAssignee"
+ @set-milestone="setMilestone"
+ @set-weight="setWeight"
/>
</form>
</gl-modal>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 8dca6be853f..849492effab 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,12 +6,13 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
-import { toggleFormEventPrefix } from '../constants';
+import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
export default {
+ draggableItemTypes: DraggableItemTypes,
name: 'BoardList',
i18n: {
loading: __('Loading'),
@@ -27,11 +28,6 @@ export default {
GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
- inject: {
- canAdminList: {
- default: false,
- },
- },
props: {
disabled: {
type: Boolean,
@@ -89,8 +85,8 @@ export default {
return !this.isEpicBoard && this.list.listType !== 'closed' && this.showIssueForm;
},
listRef() {
- // When list is draggable, the reference to the list needs to be accessed differently
- return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
+ // When list is draggable, the reference to the list needs to be accessed differently
+ return this.canMoveIssue ? this.$refs.list.$el : this.$refs.list;
},
showingAllItems() {
return this.boardItems.length === this.listItemsCount;
@@ -100,8 +96,11 @@ export default {
? this.$options.i18n.showingAllEpics
: this.$options.i18n.showingAllIssues;
},
+ canMoveIssue() {
+ return !this.disabled;
+ },
treeRootWrapper() {
- return this.canAdminList && !this.listsFlags[this.list.id]?.addItemToListInProgress
+ return this.canMoveIssue && !this.listsFlags[this.list.id]?.addItemToListInProgress
? Draggable
: 'ul';
},
@@ -116,7 +115,7 @@ export default {
value: this.boardItems,
};
- return this.canAdminList ? options : {};
+ return this.canMoveIssue ? options : {};
},
},
watch: {
@@ -172,15 +171,33 @@ export default {
this.loadNextPage();
}
},
- handleDragOnStart() {
+ handleDragOnStart({
+ item: {
+ dataset: { draggableItemType },
+ },
+ }) {
+ if (draggableItemType !== DraggableItemTypes.card) {
+ return;
+ }
+
sortableStart();
this.track('drag_card', { label: 'board' });
},
- handleDragOnEnd(params) {
+ handleDragOnEnd({
+ newIndex: originalNewIndex,
+ oldIndex,
+ from,
+ to,
+ item: {
+ dataset: { draggableItemType, itemId, itemIid, itemPath },
+ },
+ }) {
+ if (draggableItemType !== DraggableItemTypes.card) {
+ return;
+ }
+
sortableEnd();
- const { oldIndex, from, to, item } = params;
- let { newIndex } = params;
- const { itemId, itemIid, itemPath } = item.dataset;
+ let newIndex = originalNewIndex;
let { children } = to;
let moveBeforeId;
let moveAfterId;
@@ -267,6 +284,7 @@ export default {
:index="index"
:list="list"
:item="item"
+ :data-draggable-item-type="$options.draggableItemTypes.card"
:disabled="disabled"
/>
<gl-intersection-observer @appear="onReachingListBottom">
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index caeecb25227..84c9191975e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,21 +1,19 @@
<script>
-import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
-import { __ } from '~/locale';
+
import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
+
+import BoardNewItem from './board_new_item.vue';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
- i18n: {
- cancel: __('Cancel'),
- },
components: {
+ BoardNewItem,
ProjectSelect,
- GlButton,
},
mixins: [BoardNewIssueMixin],
inject: ['groupId'],
@@ -25,106 +23,55 @@ export default {
required: true,
},
},
- data() {
- return {
- title: '',
- };
- },
computed: {
- ...mapState(['selectedProject']),
- ...mapGetters(['isGroupBoard', 'isEpicBoard']),
- /**
- * We've extended this component in EE where
- * submitButtonTitle returns a different string
- * hence this is kept as a computed prop.
- */
- submitButtonTitle() {
- return __('Create issue');
+ ...mapState(['selectedProject', 'fullPath']),
+ ...mapGetters(['isGroupBoard']),
+ formEventPrefix() {
+ return toggleFormEventPrefix.issue;
},
- disabled() {
- if (this.isGroupBoard) {
- return this.title === '' || !this.selectedProject.name;
- }
- return this.title === '';
+ disableSubmit() {
+ return this.isGroupBoard ? !this.selectedProject.name : false;
},
- inputFieldId() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `${this.list.id}-title`;
+ projectPath() {
+ return this.isGroupBoard ? this.selectedProject.fullPath : this.fullPath;
},
},
- mounted() {
- this.$refs.input.focus();
- eventHub.$on('setSelectedProject', this.setSelectedProject);
- },
methods: {
...mapActions(['addListNewIssue']),
- submit() {
- const { title } = this;
+ submit({ title }) {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
- eventHub.$emit(`scroll-board-list-${this.list.id}`);
-
return this.addListNewIssue({
+ list: this.list,
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
- projectPath: this.selectedProject.fullPath,
- ...this.extraIssueInput(),
+ projectPath: this.projectPath,
},
- list: this.list,
}).then(() => {
- this.reset();
+ this.cancel();
});
},
- reset() {
- this.title = '';
- eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ cancel() {
+ eventHub.$emit(`${this.formEventPrefix}${this.list.id}`);
},
},
};
</script>
<template>
- <div class="board-new-issue-form">
- <div class="board-card position-relative p-3 rounded">
- <form ref="submitForm" @submit.prevent="submit">
- <label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label>
- <input
- :id="inputFieldId"
- ref="input"
- v-model="title"
- class="form-control"
- type="text"
- name="issue_title"
- autocomplete="off"
- />
- <project-select v-if="isGroupBoard && !isEpicBoard" :group-id="groupId" :list="list" />
- <div class="clearfix gl-mt-3">
- <gl-button
- ref="submitButton"
- :disabled="disabled"
- class="float-left js-no-auto-disable"
- variant="confirm"
- category="primary"
- type="submit"
- >
- {{ submitButtonTitle }}
- </gl-button>
- <gl-button
- ref="cancelButton"
- class="float-right"
- type="button"
- variant="default"
- @click="reset"
- >
- {{ $options.i18n.cancel }}
- </gl-button>
- </div>
- </form>
- </div>
- </div>
+ <board-new-item
+ :list="list"
+ :form-event-prefix="formEventPrefix"
+ :submit-button-title="__('Create issue')"
+ :disable-submit="disableSubmit"
+ @form-submit="submit"
+ @form-cancel="cancel"
+ >
+ <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
+ </board-new-item>
</template>
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 1218941065f..a25b436b8de 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -11,7 +11,7 @@ import ProjectSelect from './project_select_deprecated.vue';
// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
export default {
- name: 'BoardNewIssue',
+ name: 'BoardNewIssueDeprecated',
components: {
ProjectSelect,
GlButton,
diff --git a/app/assets/javascripts/boards/components/board_new_item.vue b/app/assets/javascripts/boards/components/board_new_item.vue
new file mode 100644
index 00000000000..44574de17d7
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_item.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlForm, GlFormInput, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+import eventHub from '../eventhub';
+
+export default {
+ i18n: {
+ cancel: __('Cancel'),
+ },
+ components: {
+ GlForm,
+ GlFormInput,
+ GlButton,
+ },
+ props: {
+ list: {
+ type: Object,
+ required: true,
+ },
+ formEventPrefix: {
+ type: String,
+ required: true,
+ },
+ disableSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ submitButtonTitle: {
+ type: String,
+ required: false,
+ default: __('Create issue'),
+ },
+ },
+ data() {
+ return {
+ title: '',
+ };
+ },
+ computed: {
+ inputFieldId() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.list.id}-title`;
+ },
+ },
+ methods: {
+ handleFormCancel() {
+ this.title = '';
+ this.$emit('form-cancel');
+ },
+ handleFormSubmit() {
+ const { title, list } = this;
+
+ eventHub.$emit(`scroll-board-list-${this.list.id}`);
+ this.$emit('form-submit', {
+ title,
+ list,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="board-new-issue-form">
+ <div class="board-card position-relative gl-p-5 rounded">
+ <gl-form @submit.prevent="handleFormSubmit" @reset="handleFormCancel">
+ <label :for="inputFieldId" class="gl-font-weight-bold">{{ __('Title') }}</label>
+ <gl-form-input
+ :id="inputFieldId"
+ v-model.trim="title"
+ :autofocus="true"
+ autocomplete="off"
+ type="text"
+ name="issue_title"
+ />
+ <slot></slot>
+ <div class="gl-clearfix gl-mt-4">
+ <gl-button
+ :disabled="!title || disableSubmit"
+ class="gl-float-left js-no-auto-disable"
+ variant="confirm"
+ type="submit"
+ >
+ {{ submitButtonTitle }}
+ </gl-button>
+ <gl-button class="gl-float-right js-no-auto-disable" type="reset">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index d8dac17d326..5206db05410 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,4 +1,6 @@
<script>
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import { mapActions } from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
@@ -6,13 +8,24 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_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';
export default {
+ types: {
+ ISSUE: 'ISSUE',
+ INCIDENT: 'INCIDENT',
+ },
i18n: {
search: __('Search'),
label: __('Label'),
author: __('Author'),
assignee: __('Assignee'),
+ type: __('Type'),
+ incident: __('Incident'),
+ issue: __('Issue'),
+ milestone: __('Milestone'),
+ weight: __('Weight'),
is: __('is'),
isNot: __('is not'),
},
@@ -29,7 +42,19 @@ export default {
},
computed: {
tokens() {
- const { label, is, isNot, author, assignee } = this.$options.i18n;
+ const {
+ label,
+ is,
+ isNot,
+ author,
+ assignee,
+ issue,
+ incident,
+ type,
+ milestone,
+ weight,
+ } = this.$options.i18n;
+ const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
@@ -77,10 +102,40 @@ export default {
fetchAuthors,
preloadedAuthors: this.preloadedAuthors(),
},
+ {
+ icon: 'issues',
+ title: type,
+ type: 'types',
+ operators: [{ value: '=', description: is }],
+ token: GlFilteredSearchToken,
+ unique: true,
+ options: [
+ { icon: 'issue-type-issue', value: types.ISSUE, title: issue },
+ { icon: 'issue-type-incident', value: types.INCIDENT, title: incident },
+ ],
+ },
+ {
+ type: 'milestone_title',
+ title: milestone,
+ icon: 'clock',
+ symbol: '%',
+ token: MilestoneToken,
+ unique: true,
+ defaultMilestones: [], // todo: https://gitlab.com/gitlab-org/gitlab/-/issues/337044#note_640010094
+ fetchMilestones: this.fetchMilestones,
+ },
+ {
+ type: 'weight',
+ title: weight,
+ icon: 'weight',
+ token: WeightToken,
+ unique: true,
+ },
];
},
},
methods: {
+ ...mapActions(['fetchMilestones']),
preloadedAuthors() {
return gon?.current_user_id
? [
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 84802650dad..e7696b8d31b 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -87,7 +87,7 @@ export default {
<div>
<header
v-show="showHeader"
- class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-mb-3"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-mb-2"
>
<span class="gl-vertical-align-middle">
<slot name="title">
@@ -97,7 +97,8 @@ export default {
</span>
<gl-button
v-if="canUpdate"
- variant="link"
+ category="tertiary"
+ size="small"
class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link"
data-testid="edit-button"
@click="toggle"
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 29febd0fa51..e74463825c5 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
@@ -25,6 +25,8 @@ export default {
data() {
return {
loading: false,
+ oldIid: null,
+ isEditing: false,
};
},
computed: {
@@ -72,6 +74,15 @@ export default {
return this.labelsFetchPath || projectLabelsFetchPath;
},
},
+ watch: {
+ activeBoardItem(_, oldVal) {
+ if (this.isEditing) {
+ this.oldIid = oldVal.iid;
+ } else {
+ this.oldIid = null;
+ }
+ },
+ },
methods: {
...mapActions(['setActiveBoardItemLabels', 'setError']),
async setLabels(payload) {
@@ -84,8 +95,14 @@ export default {
.filter((label) => !payload.find((selected) => selected.id === label.id))
.map((label) => label.id);
- const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
+ const input = {
+ addLabelIds,
+ removeLabelIds,
+ projectPath: this.projectPathForActiveIssue,
+ iid: this.oldIid,
+ };
await this.setActiveBoardItemLabels(input);
+ this.oldIid = null;
} catch (e) {
this.setError({ error: e, message: __('An error occurred while updating labels.') });
} finally {
@@ -115,6 +132,8 @@ export default {
:title="__('Labels')"
:loading="loading"
data-testid="sidebar-labels"
+ @open="isEditing = true"
+ @close="isEditing = false"
>
<template #collapsed>
<gl-label
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 21ef70582a4..16fb4596726 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -109,9 +109,16 @@ export const FilterFields = {
'myReactionEmoji',
'releaseTag',
'search',
+ 'types',
+ 'weight',
],
};
+export const DraggableItemTypes = {
+ card: 'card',
+ list: 'list',
+};
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/graphql/board_lists.query.graphql b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
index eb922f162f8..734867c77e9 100644
--- a/app/assets/javascripts/boards/graphql/board_lists.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists.query.graphql
@@ -9,6 +9,7 @@ query ListIssues(
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) {
+ hideBacklogList
lists(issueFilters: $filters) {
nodes {
...BoardListFragment
@@ -18,6 +19,7 @@ query ListIssues(
}
project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) {
+ hideBacklogList
lists(issueFilters: $filters) {
nodes {
...BoardListFragment
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
index 3b8c5389725..d3251c2aa12 100644
--- a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
@@ -3,7 +3,7 @@
query GroupBoardMembers($fullPath: ID!, $search: String) {
workspace: group(fullPath: $fullPath) {
__typename
- assignees: groupMembers(search: $search) {
+ assignees: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
__typename
nodes {
id
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
new file mode 100644
index 00000000000..73aa9137dec
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ group(fullPath: $fullPath) {
+ milestones(includeAncestors: true, searchTitle: $searchTerm) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
new file mode 100644
index 00000000000..8dd4d256caa
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -0,0 +1,10 @@
+query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
+ project(fullPath: $fullPath) {
+ milestones(searchTitle: $searchTerm, includeAncestors: true) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 7f655091cd0..7d6179a8547 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -11,7 +11,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
});
export default (params = {}) => {
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 0f1b72146c9..970d00841bd 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,4 +1,5 @@
import * as Sentry from '@sentry/browser';
+import { sortBy } from 'lodash';
import {
BoardType,
ListType,
@@ -13,14 +14,14 @@ import {
issuableTypes,
FilterFields,
ListTypeTitles,
+ DraggableItemTypes,
} 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 } from '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { urlParamsToObject } from '~/lib/utils/url_utility';
+import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
formatBoardLists,
@@ -35,10 +36,13 @@ import {
filterVariables,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
+import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
+
import * as types from './mutation_types';
export const gqlClient = createGqClient(
@@ -76,8 +80,7 @@ export default {
performSearch({ dispatch }) {
dispatch(
'setFilters',
- // eslint-disable-next-line import/no-deprecated
- convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
+ convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
if (gon.features.graphqlBoardLists) {
@@ -215,34 +218,99 @@ export default {
});
},
+ fetchMilestones({ state, commit }, searchTerm) {
+ commit(types.RECEIVE_MILESTONES_REQUEST);
+
+ const { fullPath, boardType } = state;
+
+ const variables = {
+ fullPath,
+ searchTerm,
+ };
+
+ let query;
+ if (boardType === BoardType.project) {
+ query = projectBoardMilestonesQuery;
+ }
+ if (boardType === BoardType.group) {
+ query = groupBoardMilestonesQuery;
+ }
+
+ if (!query) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unknown board type');
+ }
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ const errors = data[boardType]?.errors;
+ const milestones = data[boardType]?.milestones.nodes;
+
+ if (errors?.[0]) {
+ throw new Error(errors[0]);
+ }
+
+ commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
+
+ return milestones;
+ })
+ .catch((e) => {
+ commit(types.RECEIVE_MILESTONES_FAILURE);
+ throw e;
+ });
+ },
+
moveList: (
- { state, commit, dispatch },
- { listId, replacedListId, newIndex, adjustmentValue },
+ { state: { boardLists }, commit, dispatch },
+ {
+ item: {
+ dataset: { listId: movedListId, draggableItemType },
+ },
+ newIndex,
+ to: { children },
+ },
) => {
- if (listId === replacedListId) {
+ if (draggableItemType !== DraggableItemTypes.list) {
return;
}
- const { boardLists } = state;
- const backupList = { ...boardLists };
- const movedList = boardLists[listId];
+ const displacedListId = children[newIndex].dataset.listId;
+ if (movedListId === displacedListId) {
+ return;
+ }
- const newPosition = newIndex - 1;
- const listAtNewIndex = boardLists[replacedListId];
+ const listIds = sortBy(
+ Object.keys(boardLists).filter(
+ (listId) =>
+ listId !== movedListId &&
+ boardLists[listId].listType !== ListType.backlog &&
+ boardLists[listId].listType !== ListType.closed,
+ ),
+ (i) => boardLists[i].position,
+ );
- movedList.position = newPosition;
- listAtNewIndex.position += adjustmentValue;
- commit(types.MOVE_LIST, {
- movedList,
- listAtNewIndex,
- });
+ const targetPosition = boardLists[displacedListId].position;
+ // When the dragged list moves left, displaced list should shift right.
+ const shiftOffset = Number(boardLists[movedListId].position < targetPosition);
+ const displacedListIndex = listIds.findIndex((listId) => listId === displacedListId);
- dispatch('updateList', { listId, position: newPosition, backupList });
+ commit(
+ types.MOVE_LISTS,
+ listIds
+ .slice(0, displacedListIndex + shiftOffset)
+ .concat([movedListId], listIds.slice(displacedListIndex + shiftOffset))
+ .map((listId, index) => ({ listId, position: index })),
+ );
+ dispatch('updateList', { listId: movedListId, position: targetPosition });
},
updateList: (
- { commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
- { listId, position, collapsed, backupList },
+ { state: { issuableType, boardItemsByListId = {} }, dispatch },
+ { listId, position, collapsed },
) => {
gqlClient
.mutate({
@@ -255,8 +323,7 @@ export default {
})
.then(({ data }) => {
if (data?.updateBoardList?.errors.length) {
- commit(types.UPDATE_LIST_FAILURE, backupList);
- return;
+ throw new Error();
}
// Only fetch when board items havent been fetched on a collapsed list
@@ -265,10 +332,19 @@ export default {
}
})
.catch(() => {
- commit(types.UPDATE_LIST_FAILURE, backupList);
+ dispatch('handleUpdateListFailure');
});
},
+ handleUpdateListFailure: ({ dispatch, commit }) => {
+ dispatch('fetchLists');
+
+ commit(
+ types.SET_ERROR,
+ s__('Boards|An error occurred while updating the board list. Please try again.'),
+ );
+ },
+
toggleListCollapsed: ({ commit }, { listId, collapsed }) => {
commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
},
@@ -551,7 +627,7 @@ export default {
mutation: issueSetLabelsMutation,
variables: {
input: {
- iid: String(activeBoardItem.iid),
+ iid: input.iid || String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
@@ -564,7 +640,7 @@ export default {
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
+ itemId: getIdFromGraphQLId(data.updateIssue?.issue?.id) || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 49c40c7776a..857b0912c57 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,8 +8,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { mergeUrlParams, urlParamsToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
+import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
@@ -597,8 +596,7 @@ const boardsStore = {
getListIssues(list, emptyIssues = true) {
const data = {
- // eslint-disable-next-line import/no-deprecated
- ...urlParamsToObject(this.filter.path),
+ ...queryToObject(this.filter.path, { gatherArrays: true }),
page: list.page,
};
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 38c54bc8c5d..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -10,8 +10,7 @@ export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
-export const MOVE_LIST = 'MOVE_LIST';
-export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
+export const MOVE_LISTS = 'MOVE_LISTS';
export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
export const REMOVE_LIST = 'REMOVE_LIST';
export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
@@ -19,6 +18,9 @@ export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
+export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
+export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
+export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index a32a100fa11..668a3dbaa7e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,7 +1,7 @@
-import { pull, union } from 'lodash';
+import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
@@ -103,15 +103,12 @@ export default {
Vue.set(state.boardLists, list.id, list);
},
- [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => {
- const { boardLists } = state;
- Vue.set(boardLists, movedList.id, movedList);
- Vue.set(boardLists, listAtNewIndex.id, listAtNewIndex);
- },
-
- [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => {
- state.error = s__('Boards|An error occurred while updating the list. Please try again.');
- Vue.set(state, 'boardLists', backupList);
+ [mutationTypes.MOVE_LISTS]: (state, movedLists) => {
+ const updatedBoardList = movedLists.reduce((acc, { listId, position }) => {
+ acc[listId].position = position;
+ return acc;
+ }, cloneDeep(state.boardLists));
+ Vue.set(state, 'boardLists', updatedBoardList);
},
[mutationTypes.TOGGLE_LIST_COLLAPSED]: (state, { listId, collapsed }) => {
@@ -136,6 +133,20 @@ export default {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
+ [mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
+ state.milestones = milestones;
+ state.milestonesLoading = false;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
+ state.milestonesLoading = true;
+ },
+
+ [mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
+ state.milestonesLoading = false;
+ state.error = __('Failed to load milestones.');
+ },
+
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
const { listData, boardItems } = listItems;
Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 7be5ae8b583..264a03ff39d 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -19,6 +19,8 @@ export default () => ({
boardConfig: {},
labelsLoading: false,
labels: [],
+ milestones: [],
+ milestonesLoading: false,
highlightedLists: [],
selectedBoardItems: [],
groupProjects: [],
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 12def6e7eef..03fd600e493 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -24,6 +24,7 @@ import {
ADD_CI_VARIABLE_MODAL_ID,
AWS_TIP_DISMISSED_COOKIE_NAME,
AWS_TIP_MESSAGE,
+ CONTAINS_VARIABLE_REFERENCE_MESSAGE,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@@ -33,6 +34,7 @@ export default {
tokens: awsTokens,
tokenList: awsTokenList,
awsTipMessage: AWS_TIP_MESSAGE,
+ containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
components: {
CiEnvironmentsDropdown,
GlAlert,
@@ -70,6 +72,7 @@ export default {
'awsTipDeployLink',
'awsTipCommandsLink',
'awsTipLearnLink',
+ 'containsVariableReferenceLink',
'protectedEnvironmentVariablesLink',
'maskedEnvironmentVariablesLink',
]),
@@ -99,6 +102,10 @@ export default {
const regex = RegExp(this.maskableRegex);
return regex.test(this.variable.secret_value);
},
+ containsVariableReference() {
+ const regex = RegExp(/\$/);
+ return regex.test(this.variable.secret_value);
+ },
displayMaskedError() {
return !this.canMask && this.variable.masked;
},
@@ -328,6 +335,22 @@ export default {
</div>
</gl-alert>
</gl-collapse>
+ <gl-alert
+ v-if="containsVariableReference"
+ :title="__('Value may contain a variable reference')"
+ :dismissible="false"
+ variant="warning"
+ data-testid="contains-variable-reference"
+ >
+ <gl-sprintf :message="$options.containsVariableReferenceMessage">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ <template #docsLink="{ content }">
+ <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<template #modal-footer>
<gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
<gl-button
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index 564e1d01242..f4002537f79 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -24,3 +24,7 @@ export const AWS_ACCESS_KEY_ID = 'AWS_ACCESS_KEY_ID';
export const AWS_DEFAULT_REGION = 'AWS_DEFAULT_REGION';
export const AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';
export const AWS_TOKEN_CONSTANTS = [AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY];
+
+export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __(
+ 'Variable references indicated by %{codeStart}$%{codeEnd} may be expanded. If this is not what you want, consider %{docsLinkStart}using a workaround to prevent expansion%{docsLinkEnd}.',
+);
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 50856ca9533..7c40f8134d4 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -14,6 +14,7 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink,
awsTipCommandsLink,
awsTipLearnLink,
+ containsVariableReferenceLink,
protectedEnvironmentVariablesLink,
maskedEnvironmentVariablesLink,
} = containerEl.dataset;
@@ -30,6 +31,7 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink,
awsTipCommandsLink,
awsTipLearnLink,
+ containsVariableReferenceLink,
protectedEnvironmentVariablesLink,
maskedEnvironmentVariablesLink,
});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 42d46dc3d5d..b92f3d5a97b 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -2,6 +2,7 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/url_utility';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
+import { PipelineKeyOptions } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
@@ -10,6 +11,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
+ PipelineKeyOptions,
components: {
GlButton,
GlEmptyState,
@@ -205,6 +207,7 @@ export default {
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
+ :pipeline-key-option="$options.PipelineKeyOptions[0]"
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js
index 23647d99656..cd24a503631 100644
--- a/app/assets/javascripts/commons/vue.js
+++ b/app/assets/javascripts/commons/vue.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
+import Translate from '~/vue_shared/translate';
if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false;
}
Vue.use(GlFeatureFlagsPlugin);
+Vue.use(Translate);
Vue.config.ignoredElements = ['gl-emoji'];
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 9a51def7075..a372233e543 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,45 +1,111 @@
<script>
-import { GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
-import { ContentEditor } from '../services/content_editor';
+import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
+import { createContentEditor } from '../services/create_content_editor';
+import ContentEditorError from './content_editor_error.vue';
+import ContentEditorProvider from './content_editor_provider.vue';
+import EditorStateObserver from './editor_state_observer.vue';
+import FormattingBubbleMenu from './formatting_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
export default {
components: {
- GlAlert,
+ GlLoadingIcon,
+ ContentEditorError,
+ ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
+ FormattingBubbleMenu,
+ EditorStateObserver,
},
props: {
- contentEditor: {
- type: ContentEditor,
+ renderMarkdown: {
+ type: Function,
required: true,
},
+ uploadsPath: {
+ type: String,
+ required: true,
+ },
+ extensions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ serializerConfig: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
data() {
return {
- error: '',
+ isLoadingContent: false,
+ focused: false,
};
},
- mounted() {
- this.contentEditor.tiptapEditor.on('error', (error) => {
- this.error = error;
+ created() {
+ const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this;
+
+ // This is a non-reactive attribute intentionally since this is a complex object.
+ this.contentEditor = createContentEditor({
+ renderMarkdown,
+ uploadsPath,
+ extensions,
+ serializerConfig,
});
+
+ this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
+ this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
+ this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
+ this.$emit('initialized', this.contentEditor);
+ },
+ beforeDestroy() {
+ this.contentEditor.dispose();
+ this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
+ this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
+ this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
+ },
+ methods: {
+ displayLoadingIndicator() {
+ this.isLoadingContent = true;
+ },
+ hideLoadingIndicator() {
+ this.isLoadingContent = false;
+ },
+ focus() {
+ this.focused = true;
+ },
+ blur() {
+ this.focused = false;
+ },
+ notifyChange() {
+ this.$emit('change', {
+ empty: this.contentEditor.empty,
+ });
+ },
},
};
</script>
<template>
- <div>
- <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="error = ''">
- {{ error }}
- </gl-alert>
- <div
- data-testid="content-editor"
- class="md-area"
- :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
- >
- <top-toolbar ref="toolbar" class="gl-mb-4" :content-editor="contentEditor" />
- <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ <content-editor-provider :content-editor="contentEditor">
+ <div>
+ <editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
+ <content-editor-error />
+ <div
+ data-testid="content-editor"
+ data-qa-selector="content_editor_container"
+ class="md-area"
+ :class="{ 'is-focused': focused }"
+ >
+ <top-toolbar ref="toolbar" class="gl-mb-4" />
+ <formatting-bubble-menu />
+ <div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
+ <gl-loading-icon size="sm" />
+ </div>
+ <tiptap-editor-content v-else class="md" :editor="contentEditor.tiptapEditor" />
+ </div>
</div>
- </div>
+ </content-editor-provider>
</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_error.vue b/app/assets/javascripts/content_editor/components/content_editor_error.vue
new file mode 100644
index 00000000000..031ea92a7e9
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_error.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import EditorStateObserver from './editor_state_observer.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EditorStateObserver,
+ },
+ data() {
+ return {
+ error: null,
+ };
+ },
+ methods: {
+ displayError({ error }) {
+ this.error = error;
+ },
+ dismissError() {
+ this.error = null;
+ },
+ },
+};
+</script>
+<template>
+ <editor-state-observer @error="displayError">
+ <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
+ {{ error }}
+ </gl-alert>
+ </editor-state-observer>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
new file mode 100644
index 00000000000..630aff9858f
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -0,0 +1,24 @@
+<script>
+export default {
+ provide() {
+ // We can't use this.contentEditor due to bug in vue-apollo when
+ // provide is called in beforeCreate
+ // See https://github.com/vuejs/vue-apollo/pull/1153 for details
+ const { contentEditor } = this.$options.propsData;
+
+ return {
+ contentEditor,
+ tiptapEditor: contentEditor.tiptapEditor,
+ };
+ },
+ props: {
+ contentEditor: {
+ type: Object,
+ required: true,
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
new file mode 100644
index 00000000000..2eeb0719096
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -0,0 +1,40 @@
+<script>
+import { debounce } from 'lodash';
+
+export const tiptapToComponentMap = {
+ update: 'docUpdate',
+ selectionUpdate: 'selectionUpdate',
+ transaction: 'transaction',
+ focus: 'focus',
+ blur: 'blur',
+ error: 'error',
+};
+
+const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
+
+export default {
+ inject: ['tiptapEditor'],
+ created() {
+ this.disposables = [];
+
+ Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
+ const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100);
+
+ this.tiptapEditor?.on(tiptapEvent, eventHandler);
+
+ this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
+ });
+ },
+ beforeDestroy() {
+ this.disposables.forEach((dispose) => dispose());
+ },
+ methods: {
+ handleTipTapEvent(tiptapEvent, params) {
+ this.$emit(getComponentEventName(tiptapEvent), params);
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
new file mode 100644
index 00000000000..6c00480b87e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlButtonGroup } from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+import { BUBBLE_MENU_TRACKING_ACTION } from '../constants';
+import trackUIControl from '../services/track_ui_control';
+import ToolbarButton from './toolbar_button.vue';
+
+export default {
+ components: {
+ BubbleMenu,
+ GlButtonGroup,
+ ToolbarButton,
+ },
+ inject: ['tiptapEditor'],
+ methods: {
+ trackToolbarControlExecution({ contentType, value }) {
+ trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu class="gl-shadow gl-rounded-base" :editor="tiptapEditor">
+ <gl-button-group>
+ <toolbar-button
+ data-testid="bold"
+ content-type="bold"
+ icon-name="bold"
+ editor-command="toggleBold"
+ category="primary"
+ size="medium"
+ :label="__('Bold text')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="italic"
+ content-type="italic"
+ icon-name="italic"
+ editor-command="toggleItalic"
+ category="primary"
+ size="medium"
+ :label="__('Italic text')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="strike"
+ content-type="strike"
+ icon-name="strikethrough"
+ editor-command="toggleStrike"
+ category="primary"
+ size="medium"
+ :label="__('Strikethrough')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="code"
+ content-type="code"
+ icon-name="code"
+ editor-command="toggleCode"
+ category="primary"
+ size="medium"
+ :label="__('Code')"
+ @execute="trackToolbarControlExecution"
+ />
+ </gl-button-group>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
index 0af12812f3b..cdb877152d4 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -1,23 +1,21 @@
<script>
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
GlButton,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
+ inject: ['tiptapEditor'],
props: {
iconName: {
type: String,
required: true,
},
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
contentType: {
type: String,
required: true,
@@ -31,13 +29,31 @@ export default {
required: false,
default: '',
},
- },
- computed: {
- isActive() {
- return this.tiptapEditor.isActive(this.contentType) && this.tiptapEditor.isFocused;
+ variant: {
+ type: String,
+ required: false,
+ default: 'default',
},
+ category: {
+ type: String,
+ required: false,
+ default: 'tertiary',
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'small',
+ },
+ },
+ data() {
+ return {
+ isActive: null,
+ };
},
methods: {
+ updateActive({ editor }) {
+ this.isActive = editor.isActive(this.contentType) && editor.isFocused;
+ },
execute() {
const { contentType } = this;
@@ -51,15 +67,17 @@ export default {
};
</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"
- />
+ <editor-state-observer @transaction="updateActive">
+ <gl-button
+ v-gl-tooltip
+ :variant="variant"
+ :category="category"
+ :size="size"
+ :class="{ active: isActive }"
+ :aria-label="label"
+ :title="label"
+ :icon="iconName"
+ @click="execute"
+ />
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_image_button.vue b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
index ebeee16dbec..649e23c29aa 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
@@ -8,9 +8,8 @@ import {
GlDropdownItem,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
-import { acceptedMimes } from '../extensions/image';
-import { getImageAlt } from '../services/utils';
+import { acceptedMimes } from '../services/upload_helpers';
+import { extractFilename } from '../services/utils';
export default {
components: {
@@ -24,12 +23,7 @@ export default {
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
- },
+ inject: ['tiptapEditor'],
data() {
return {
imgSrc: '',
@@ -47,7 +41,7 @@ export default {
.setImage({
src: this.imgSrc,
canonicalSrc: this.imgSrc,
- alt: getImageAlt(this.imgSrc),
+ alt: extractFilename(this.imgSrc),
})
.run();
@@ -64,7 +58,7 @@ export default {
this.tiptapEditor
.chain()
.focus()
- .uploadImage({
+ .uploadAttachment({
file: e.target.files[0],
})
.run();
@@ -73,7 +67,7 @@ export default {
this.emitExecute('upload');
},
},
- acceptedMimes,
+ acceptedMimes: acceptedMimes.image,
};
</script>
<template>
@@ -104,6 +98,7 @@ export default {
name="content_editor_image"
:accept="$options.acceptedMimes"
class="gl-display-none"
+ data-qa-selector="file_upload_field"
@change="onFileSelect"
/>
</gl-dropdown>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
index 8f57959a73f..ff525e52873 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
@@ -8,10 +8,9 @@ import {
GlDropdownItem,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import Link from '../extensions/link';
import { hasSelection } from '../services/utils';
-
-export const linkContentType = 'link';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
@@ -21,34 +20,32 @@ export default {
GlDropdownDivider,
GlDropdownItem,
GlButton,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
- },
+ inject: ['tiptapEditor'],
data() {
return {
linkHref: '',
+ isActive: false,
};
},
- computed: {
- isActive() {
- return this.tiptapEditor.isActive(linkContentType);
+ methods: {
+ resetFields() {
+ this.imgSrc = '';
+ this.$refs.fileSelector.value = '';
},
- },
- mounted() {
- this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
- const { canonicalSrc, href } = editor.getAttributes(linkContentType);
+ openFileUpload() {
+ this.$refs.fileSelector.click();
+ },
+ updateLinkState({ editor }) {
+ const { canonicalSrc, href } = editor.getAttributes(Link.name);
+ this.isActive = editor.isActive(Link.name);
this.linkHref = canonicalSrc || href;
- });
- },
- methods: {
+ },
updateLink() {
this.tiptapEditor
.chain()
@@ -60,45 +57,70 @@ export default {
})
.run();
- this.$emit('execute', { contentType: linkContentType });
+ this.$emit('execute', { contentType: Link.name });
},
selectLink() {
const { tiptapEditor } = this;
// a selection has already been made by the user, so do nothing
if (!hasSelection(tiptapEditor)) {
- tiptapEditor.chain().focus().extendMarkRange(linkContentType).run();
+ tiptapEditor.chain().focus().extendMarkRange(Link.name).run();
}
},
removeLink() {
this.tiptapEditor.chain().focus().unsetLink().run();
- this.$emit('execute', { contentType: linkContentType });
+ this.$emit('execute', { contentType: Link.name });
+ },
+ onFileSelect(e) {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .uploadAttachment({
+ file: e.target.files[0],
+ })
+ .run();
+
+ this.resetFields();
+ this.$emit('execute', { contentType: Link.name });
},
},
};
</script>
<template>
- <gl-dropdown
- v-gl-tooltip
- :aria-label="__('Insert link')"
- :title="__('Insert link')"
- :toggle-class="{ active: isActive }"
- size="small"
- category="tertiary"
- icon="link"
- @show="selectLink()"
- >
- <gl-dropdown-form class="gl-px-3!">
- <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
- <template #append>
- <gl-button variant="confirm" @click="updateLink()">{{ __('Apply') }}</gl-button>
- </template>
- </gl-form-input-group>
- </gl-dropdown-form>
- <gl-dropdown-divider v-if="isActive" />
- <gl-dropdown-item v-if="isActive" @click="removeLink()">
- {{ __('Remove link') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <editor-state-observer @transaction="updateLinkState">
+ <gl-dropdown
+ v-gl-tooltip
+ :aria-label="__('Insert link')"
+ :title="__('Insert link')"
+ :toggle-class="{ active: isActive }"
+ size="small"
+ category="tertiary"
+ icon="link"
+ @show="selectLink()"
+ >
+ <gl-dropdown-form class="gl-px-3!">
+ <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
+ <template #append>
+ <gl-button variant="confirm" @click="updateLink">{{ __('Apply') }}</gl-button>
+ </template>
+ </gl-form-input-group>
+ </gl-dropdown-form>
+ <gl-dropdown-divider />
+ <gl-dropdown-item v-if="isActive" @click="removeLink">
+ {{ __('Remove link') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-else @click="openFileUpload">
+ {{ __('Upload file') }}
+ </gl-dropdown-item>
+
+ <input
+ ref="fileSelector"
+ type="file"
+ name="content_editor_attachment"
+ class="gl-display-none"
+ @change="onFileSelect"
+ />
+ </gl-dropdown>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
index 49d3006e9bf..46db806da94 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
@@ -1,29 +1,23 @@
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __, sprintf } from '~/locale';
import { clamp } from '../services/utils';
export const tableContentType = 'table';
-const MIN_ROWS = 3;
-const MIN_COLS = 3;
-const MAX_ROWS = 8;
-const MAX_COLS = 8;
+const MIN_ROWS = 5;
+const MIN_COLS = 5;
+const MAX_ROWS = 10;
+const MAX_COLS = 10;
export default {
components: {
+ GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownForm,
- GlButton,
- },
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
},
+ inject: ['tiptapEditor'],
data() {
return {
maxRows: MIN_ROWS,
@@ -68,22 +62,22 @@ export default {
};
</script>
<template>
- <gl-dropdown size="small" category="tertiary" icon="table">
- <gl-dropdown-form class="gl-px-3! gl-w-auto!">
- <div class="gl-w-auto!">
- <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
- <gl-button
- v-for="c of list(maxCols)"
- :key="c"
- :data-testid="`table-${r}-${c}`"
- :class="{ 'gl-bg-blue-50!': r <= rows && c <= cols }"
- :aria-label="getButtonLabel(r, c)"
- class="gl-display-inline! gl-px-0! gl-w-5! gl-h-5! gl-rounded-0!"
- @mouseover="setRowsAndCols(r, c)"
- @click="insertTable()"
- />
- </div>
- <gl-dropdown-divider />
+ <gl-dropdown size="small" category="tertiary" icon="table" class="table-dropdown">
+ <gl-dropdown-form class="gl-px-3!">
+ <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
+ <gl-button
+ v-for="c of list(maxCols)"
+ :key="c"
+ :data-testid="`table-${r}-${c}`"
+ :class="{ 'active gl-bg-blue-50!': r <= rows && c <= cols }"
+ :aria-label="getButtonLabel(r, c)"
+ class="table-creator-grid-item gl-display-inline gl-rounded-0! gl-w-6! gl-h-6! gl-p-0!"
+ @mouseover="setRowsAndCols(r, c)"
+ @click="insertTable()"
+ />
+ </div>
+ <gl-dropdown-divider class="gl-my-3! gl-mx-n3!" />
+ <div class="gl-px-1">
{{ getButtonLabel(rows, cols) }}
</div>
</gl-dropdown-form>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
index 473fc472c1b..13728d4001d 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
@@ -1,29 +1,25 @@
<script>
import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '../constants';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
+ inject: ['tiptapEditor'],
+ data() {
+ return {
+ activeItem: null,
+ };
},
computed: {
- activeItem() {
- return TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
- this.tiptapEditor.isActive(item.contentType, item.commandParams),
- );
- },
activeItemLabel() {
const { activeItem } = this;
@@ -31,6 +27,11 @@ export default {
},
},
methods: {
+ updateActiveItem({ editor }) {
+ this.activeItem = TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
+ editor.isActive(item.contentType, item.commandParams),
+ );
+ },
execute(item) {
const { editorCommand, contentType, commandParams } = item;
const value = commandParams?.level;
@@ -38,8 +39,8 @@ export default {
if (editorCommand) {
this.tiptapEditor
.chain()
- .focus()
[editorCommand](commandParams || {})
+ .focus()
.run();
}
@@ -56,20 +57,25 @@ export default {
};
</script>
<template>
- <gl-dropdown
- v-gl-tooltip="$options.i18n.placeholder"
- size="small"
- :disabled="!activeItem"
- :text="activeItemLabel"
- >
- <gl-dropdown-item
- v-for="(item, index) in $options.items"
- :key="index"
- is-check-item
- :is-checked="isActive(item)"
- @click="execute(item)"
+ <editor-state-observer @transaction="updateActiveItem">
+ <gl-dropdown
+ v-gl-tooltip="$options.i18n.placeholder"
+ size="small"
+ data-qa-selector="text_style_dropdown"
+ :disabled="!activeItem"
+ :text="activeItemLabel"
>
- {{ item.label }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-dropdown-item
+ v-for="(item, index) in $options.items"
+ :key="index"
+ is-check-item
+ :is-checked="isActive(item)"
+ data-qa-selector="text_style_menu_item"
+ :data-qa-text-style="item.label"
+ @click="execute(item)"
+ >
+ {{ item.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index fafc7a660e7..82a449ae6af 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -1,7 +1,5 @@
<script>
-import Tracking from '~/tracking';
-import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
-import { ContentEditor } from '../services/content_editor';
+import trackUIControl from '../services/track_ui_control';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
@@ -9,10 +7,6 @@ import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
-const trackingMixin = Tracking.mixin({
- label: CONTENT_EDITOR_TRACKING_LABEL,
-});
-
export default {
components: {
ToolbarButton,
@@ -22,19 +16,9 @@ export default {
ToolbarImageButton,
Divider,
},
- mixins: [trackingMixin],
- props: {
- contentEditor: {
- type: ContentEditor,
- required: true,
- },
- },
methods: {
- trackToolbarControlExecution({ contentType: property, value }) {
- this.track(TOOLBAR_CONTROL_TRACKING_ACTION, {
- property,
- value,
- });
+ trackToolbarControlExecution({ contentType, value }) {
+ trackUIControl({ property: contentType, value });
},
},
};
@@ -45,7 +29,6 @@ export default {
>
<toolbar-text-style-dropdown
data-testid="text-styles"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<divider />
@@ -53,99 +36,91 @@ export default {
data-testid="bold"
content-type="bold"
icon-name="bold"
+ class="gl-mx-2"
editor-command="toggleBold"
:label="__('Bold text')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="italic"
content-type="italic"
icon-name="italic"
+ class="gl-mx-2"
editor-command="toggleItalic"
:label="__('Italic text')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="strike"
content-type="strike"
icon-name="strikethrough"
+ class="gl-mx-2"
editor-command="toggleStrike"
:label="__('Strikethrough')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="code"
content-type="code"
icon-name="code"
+ class="gl-mx-2"
editor-command="toggleCode"
:label="__('Code')"
- :tiptap-editor="contentEditor.tiptapEditor"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-link-button
- data-testid="link"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" />
<divider />
<toolbar-image-button
ref="imageButton"
data-testid="image"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="blockquote"
content-type="blockquote"
icon-name="quote"
+ class="gl-mx-2"
editor-command="toggleBlockquote"
:label="__('Insert a quote')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="code-block"
content-type="codeBlock"
icon-name="doc-code"
+ class="gl-mx-2"
editor-command="toggleCodeBlock"
:label="__('Insert a code block')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="bullet-list"
content-type="bulletList"
icon-name="list-bulleted"
+ class="gl-mx-2"
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"
+ class="gl-mx-2"
editor-command="toggleOrderedList"
:label="__('Add a numbered list')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="horizontal-rule"
content-type="horizontalRule"
icon-name="dash"
+ class="gl-mx-2"
editor-command="setHorizontalRule"
:label="__('Add a horizontal rule')"
- :tiptap-editor="contentEditor.tiptapEditor"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-table-button
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-table-button @execute="trackToolbarControlExecution" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index 7a5f1d3ed1f..f277508f628 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -6,6 +6,7 @@ export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__(
export const CONTENT_EDITOR_TRACKING_LABEL = 'content_editor';
export const TOOLBAR_CONTROL_TRACKING_ACTION = 'execute_toolbar_control';
+export const BUBBLE_MENU_TRACKING_ACTION = 'execute_bubble_menu_control';
export const KEYBOARD_SHORTCUT_TRACKING_ACTION = 'execute_keyboard_shortcut';
export const INPUT_RULE_TRACKING_ACTION = 'execute_input_rule';
@@ -40,3 +41,7 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
label: __('Normal text'),
},
];
+
+export const LOADING_CONTENT_EVENT = 'loadingContent';
+export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
+export const LOADING_ERROR_EVENT = 'loadingError';
diff --git a/app/assets/javascripts/content_editor/extensions/attachment.js b/app/assets/javascripts/content_editor/extensions/attachment.js
new file mode 100644
index 00000000000..29ee282f2d2
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/attachment.js
@@ -0,0 +1,53 @@
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { handleFileEvent } from '../services/upload_helpers';
+
+export default Extension.create({
+ name: 'attachment',
+
+ defaultOptions: {
+ uploadsPath: null,
+ renderMarkdown: null,
+ },
+
+ addCommands() {
+ return {
+ uploadAttachment: ({ file }) => () => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ return handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
+ },
+ };
+ },
+ addProseMirrorPlugins() {
+ const { editor } = this;
+
+ return [
+ new Plugin({
+ key: new PluginKey('attachment'),
+ props: {
+ handlePaste: (_, event) => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ return handleFileEvent({
+ editor,
+ file: event.clipboardData.files[0],
+ uploadsPath,
+ renderMarkdown,
+ });
+ },
+ handleDrop: (_, event) => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ return handleFileEvent({
+ editor,
+ file: event.dataTransfer.files[0],
+ uploadsPath,
+ renderMarkdown,
+ });
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
index a4297b4550c..45f53fe230b 100644
--- a/app/assets/javascripts/content_editor/extensions/blockquote.js
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -1,5 +1 @@
-import { Blockquote } from '@tiptap/extension-blockquote';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Blockquote;
-export const serializer = defaultMarkdownSerializer.nodes.blockquote;
+export { Blockquote as default } from '@tiptap/extension-blockquote';
diff --git a/app/assets/javascripts/content_editor/extensions/bold.js b/app/assets/javascripts/content_editor/extensions/bold.js
index e90e7b59da0..0b7b22265b6 100644
--- a/app/assets/javascripts/content_editor/extensions/bold.js
+++ b/app/assets/javascripts/content_editor/extensions/bold.js
@@ -1,5 +1 @@
-import { Bold } from '@tiptap/extension-bold';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Bold;
-export const serializer = defaultMarkdownSerializer.marks.strong;
+export { Bold as default } from '@tiptap/extension-bold';
diff --git a/app/assets/javascripts/content_editor/extensions/bullet_list.js b/app/assets/javascripts/content_editor/extensions/bullet_list.js
index 178b798e2d4..01ead571fe1 100644
--- a/app/assets/javascripts/content_editor/extensions/bullet_list.js
+++ b/app/assets/javascripts/content_editor/extensions/bullet_list.js
@@ -1,5 +1 @@
-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;
+export { BulletList as default } from '@tiptap/extension-bullet-list';
diff --git a/app/assets/javascripts/content_editor/extensions/code.js b/app/assets/javascripts/content_editor/extensions/code.js
index 8be50dc39c5..f93c22ad10e 100644
--- a/app/assets/javascripts/content_editor/extensions/code.js
+++ b/app/assets/javascripts/content_editor/extensions/code.js
@@ -1,5 +1 @@
-import { Code } from '@tiptap/extension-code';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Code;
-export const serializer = defaultMarkdownSerializer.marks.code;
+export { Code as default } from '@tiptap/extension-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 50d72f4089a..c6d32fb8547 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,10 +1,9 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import * as lowlight from 'lowlight';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
const extractLanguage = (element) => element.getAttribute('lang');
-const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({
+export default CodeBlockLowlight.extend({
addAttributes() {
return {
language: {
@@ -15,18 +14,6 @@ const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({
};
},
},
- /* `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),
- };
- },
- },
class: {
default: 'code highlight js-syntax-highlight',
},
@@ -38,6 +25,3 @@ const ExtendedCodeBlockLowlight = CodeBlockLowlight.extend({
}).configure({
lowlight,
});
-
-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
index 99aa8d6235a..27496fd60b7 100644
--- a/app/assets/javascripts/content_editor/extensions/document.js
+++ b/app/assets/javascripts/content_editor/extensions/document.js
@@ -1,3 +1 @@
-import Document from '@tiptap/extension-document';
-
-export const tiptapExtension = Document;
+export { Document as default } from '@tiptap/extension-document';
diff --git a/app/assets/javascripts/content_editor/extensions/dropcursor.js b/app/assets/javascripts/content_editor/extensions/dropcursor.js
index 44c378ac7db..825dc73b9d9 100644
--- a/app/assets/javascripts/content_editor/extensions/dropcursor.js
+++ b/app/assets/javascripts/content_editor/extensions/dropcursor.js
@@ -1,3 +1 @@
-import Dropcursor from '@tiptap/extension-dropcursor';
-
-export const tiptapExtension = Dropcursor;
+export { Dropcursor as default } from '@tiptap/extension-dropcursor';
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
new file mode 100644
index 00000000000..d88b9f92215
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -0,0 +1,93 @@
+import { Node } from '@tiptap/core';
+import { InputRule } from 'prosemirror-inputrules';
+import { initEmojiMap, getAllEmoji } from '~/emoji';
+
+export const emojiInputRegex = /(?:^|\s)((?::)((?:\w+))(?::))$/;
+
+export default Node.create({
+ name: 'emoji',
+
+ inline: true,
+
+ group: 'inline',
+
+ draggable: true,
+
+ addAttributes() {
+ return {
+ moji: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ moji: element.textContent,
+ };
+ },
+ },
+ name: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ name: element.dataset.name,
+ };
+ },
+ },
+ title: {
+ default: null,
+ },
+ unicodeVersion: {
+ default: '6.0',
+ parseHTML: (element) => {
+ return {
+ unicodeVersion: element.dataset.unicodeVersion,
+ };
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'gl-emoji',
+ },
+ ];
+ },
+
+ renderHTML({ node }) {
+ return [
+ 'gl-emoji',
+ {
+ 'data-name': node.attrs.name,
+ title: node.attrs.title,
+ 'data-unicode-version': node.attrs.unicodeVersion,
+ },
+ node.attrs.moji,
+ ];
+ },
+
+ addInputRules() {
+ return [
+ new InputRule(emojiInputRegex, (state, match, start, end) => {
+ const [, , name] = match;
+ const emojis = getAllEmoji();
+ const emoji = emojis[name];
+ const { tr } = state;
+
+ if (emoji) {
+ tr.replaceWith(start, end, [
+ state.schema.text(' '),
+ this.type.create({ name, moji: emoji.e, unicodeVersion: emoji.u, title: emoji.d }),
+ ]);
+
+ return tr;
+ }
+
+ return null;
+ }),
+ ];
+ },
+
+ onCreate() {
+ initEmojiMap();
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/gapcursor.js b/app/assets/javascripts/content_editor/extensions/gapcursor.js
index 2db862e4580..ef88cd92b4e 100644
--- a/app/assets/javascripts/content_editor/extensions/gapcursor.js
+++ b/app/assets/javascripts/content_editor/extensions/gapcursor.js
@@ -1,3 +1 @@
-import Gapcursor from '@tiptap/extension-gapcursor';
-
-export const tiptapExtension = Gapcursor;
+export { Gapcursor as default } from '@tiptap/extension-gapcursor';
diff --git a/app/assets/javascripts/content_editor/extensions/hard_break.js b/app/assets/javascripts/content_editor/extensions/hard_break.js
index 756eefa875c..fb81c6b79b6 100644
--- a/app/assets/javascripts/content_editor/extensions/hard_break.js
+++ b/app/assets/javascripts/content_editor/extensions/hard_break.js
@@ -1,13 +1,9 @@
import { HardBreak } from '@tiptap/extension-hard-break';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-const ExtendedHardBreak = HardBreak.extend({
+export default HardBreak.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
};
},
});
-
-export const tiptapExtension = ExtendedHardBreak;
-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
index f69869d1e09..48303cdeca4 100644
--- a/app/assets/javascripts/content_editor/extensions/heading.js
+++ b/app/assets/javascripts/content_editor/extensions/heading.js
@@ -1,5 +1 @@
-import { Heading } from '@tiptap/extension-heading';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Heading;
-export const serializer = defaultMarkdownSerializer.nodes.heading;
+export { Heading as default } from '@tiptap/extension-heading';
diff --git a/app/assets/javascripts/content_editor/extensions/history.js b/app/assets/javascripts/content_editor/extensions/history.js
index 554d797d30a..7c9d92d7b4e 100644
--- a/app/assets/javascripts/content_editor/extensions/history.js
+++ b/app/assets/javascripts/content_editor/extensions/history.js
@@ -1,3 +1 @@
-import History from '@tiptap/extension-history';
-
-export const tiptapExtension = History;
+export { History as default } from '@tiptap/extension-history';
diff --git a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
index c287938af5c..c8ec45d835c 100644
--- a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
+++ b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
@@ -1,12 +1,10 @@
import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const hrInputRuleRegExp = /^---$/;
-export const tiptapExtension = HorizontalRule.extend({
+export default HorizontalRule.extend({
addInputRules() {
return [nodeInputRule(hrInputRuleRegExp, this.type)];
},
});
-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
index 4dd8a1376ad..c9e8dfa4ad9 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,58 +1,14 @@
import { Image } from '@tiptap/extension-image';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import { Plugin, PluginKey } from 'prosemirror-state';
-import { __ } from '~/locale';
import ImageWrapper from '../components/wrappers/image.vue';
-import { uploadFile } from '../services/upload_file';
-import { getImageAlt, readFileAsDataURL } from '../services/utils';
-
-export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img');
-const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
- const encodedSrc = await readFileAsDataURL(file);
- const { view } = editor;
-
- editor.commands.setImage({ uploading: true, src: encodedSrc });
-
- const { state } = view;
- const position = state.selection.from - 1;
- const { tr } = state;
-
- try {
- const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
-
- view.dispatch(
- tr.setNodeMarkup(position, undefined, {
- uploading: false,
- src: encodedSrc,
- alt: getImageAlt(src),
- canonicalSrc,
- }),
- );
- } catch (e) {
- editor.commands.deleteRange({ from: position, to: position + 1 });
- editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
- }
-};
-
-const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
- if (acceptedMimes.includes(file?.type)) {
- startFileUpload({ editor, file, uploadsPath, renderMarkdown });
-
- return true;
- }
-
- return false;
-};
-
-const ExtendedImage = Image.extend({
+export default Image.extend({
defaultOptions: {
...Image.options,
- uploadsPath: null,
- renderMarkdown: null,
+ inline: true,
},
addAttributes() {
return {
@@ -107,62 +63,7 @@ const ExtendedImage = Image.extend({
},
];
},
- addCommands() {
- return {
- ...this.parent(),
- uploadImage: ({ file }) => () => {
- const { uploadsPath, renderMarkdown } = this.options;
-
- handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
- },
- };
- },
- addProseMirrorPlugins() {
- const { editor } = this;
-
- return [
- new Plugin({
- key: new PluginKey('handleDropAndPasteImages'),
- props: {
- handlePaste: (_, event) => {
- const { uploadsPath, renderMarkdown } = this.options;
-
- return handleFileEvent({
- editor,
- file: event.clipboardData.files[0],
- uploadsPath,
- renderMarkdown,
- });
- },
- handleDrop: (_, event) => {
- const { uploadsPath, renderMarkdown } = this.options;
-
- return handleFileEvent({
- editor,
- file: event.dataTransfer.files[0],
- uploadsPath,
- renderMarkdown,
- });
- },
- },
- }),
- ];
- },
addNodeView() {
return VueNodeViewRenderer(ImageWrapper);
},
});
-
-const serializer = (state, node) => {
- const { alt, canonicalSrc, src, title } = node.attrs;
- const quotedTitle = title ? ` ${state.quote(title)}` : '';
-
- state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
-};
-
-export const configure = ({ renderMarkdown, uploadsPath }) => {
- return {
- tiptapExtension: ExtendedImage.configure({ inline: true, renderMarkdown, uploadsPath }),
- serializer,
- };
-};
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
new file mode 100644
index 00000000000..9471d324764
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -0,0 +1,50 @@
+import { Mark, markInputRule, mergeAttributes } from '@tiptap/core';
+
+export const inputRegexAddition = /(\{\+(.+?)\+\})$/gm;
+export const inputRegexDeletion = /(\{-(.+?)-\})$/gm;
+
+export default Mark.create({
+ name: 'inlineDiff',
+
+ defaultOptions: {
+ HTMLAttributes: {},
+ },
+
+ addAttributes() {
+ return {
+ type: {
+ default: 'addition',
+ parseHTML: (element) => {
+ return {
+ type: element.classList.contains('deletion') ? 'deletion' : 'addition',
+ };
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'span.idiff',
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes: { type, ...HTMLAttributes } }) {
+ return [
+ 'span',
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ class: `idiff left right ${type}`,
+ }),
+ 0,
+ ];
+ },
+
+ addInputRules() {
+ return [
+ markInputRule(inputRegexAddition, this.type, () => ({ type: 'addition' })),
+ markInputRule(inputRegexDeletion, this.type, () => ({ type: 'deletion' })),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/italic.js b/app/assets/javascripts/content_editor/extensions/italic.js
index b8a7c4aba3e..99e9922044d 100644
--- a/app/assets/javascripts/content_editor/extensions/italic.js
+++ b/app/assets/javascripts/content_editor/extensions/italic.js
@@ -1,4 +1 @@
-import { Italic } from '@tiptap/extension-italic';
-
-export const tiptapExtension = Italic;
-export const serializer = { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true };
+export { Italic as default } from '@tiptap/extension-italic';
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index 12019ab4636..53104fe07a3 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -20,7 +20,11 @@ export const extractHrefFromMarkdownLink = (match) => {
return extractHrefFromMatch(match);
};
-export const tiptapExtension = Link.extend({
+export default Link.extend({
+ defaultOptions: {
+ ...Link.options,
+ openOnClick: false,
+ },
addInputRules() {
return [
markInputRule(markdownLinkSyntaxInputRuleRegExp, this.type, extractHrefFromMarkdownLink),
@@ -48,16 +52,4 @@ export const tiptapExtension = Link.extend({
},
};
},
-}).configure({
- openOnClick: false,
});
-
-export const serializer = {
- open() {
- return '[';
- },
- close(state, mark) {
- const href = mark.attrs.canonicalSrc || mark.attrs.href;
- return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
- },
-};
diff --git a/app/assets/javascripts/content_editor/extensions/list_item.js b/app/assets/javascripts/content_editor/extensions/list_item.js
index 86da98f6df7..72454b0905d 100644
--- a/app/assets/javascripts/content_editor/extensions/list_item.js
+++ b/app/assets/javascripts/content_editor/extensions/list_item.js
@@ -1,5 +1 @@
-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;
+export { ListItem as default } from '@tiptap/extension-list-item';
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
new file mode 100644
index 00000000000..2324e9b132d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/loading.js
@@ -0,0 +1,24 @@
+import { Node } from '@tiptap/core';
+
+export default Node.create({
+ name: 'loading',
+ inline: true,
+ group: 'inline',
+
+ addAttributes() {
+ return {
+ label: {
+ default: null,
+ },
+ };
+ },
+
+ renderHTML({ node }) {
+ return [
+ 'span',
+ { class: 'gl-display-inline-flex gl-align-items-center' },
+ ['span', { class: 'gl-spinner gl-mx-2' }],
+ ['span', { class: 'gl-link' }, node.attrs.label],
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/ordered_list.js b/app/assets/javascripts/content_editor/extensions/ordered_list.js
index d980ab8bf10..9a79187d9c1 100644
--- a/app/assets/javascripts/content_editor/extensions/ordered_list.js
+++ b/app/assets/javascripts/content_editor/extensions/ordered_list.js
@@ -1,5 +1 @@
-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;
+export { OrderedList as default } from '@tiptap/extension-ordered-list';
diff --git a/app/assets/javascripts/content_editor/extensions/paragraph.js b/app/assets/javascripts/content_editor/extensions/paragraph.js
index 6c9f204b8ac..33bf1c94003 100644
--- a/app/assets/javascripts/content_editor/extensions/paragraph.js
+++ b/app/assets/javascripts/content_editor/extensions/paragraph.js
@@ -1,5 +1 @@
-import { Paragraph } from '@tiptap/extension-paragraph';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Paragraph;
-export const serializer = defaultMarkdownSerializer.nodes.paragraph;
+export { Paragraph as default } from '@tiptap/extension-paragraph';
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
new file mode 100644
index 00000000000..5f4484af9c8
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -0,0 +1,78 @@
+import { Node } from '@tiptap/core';
+
+export default Node.create({
+ name: 'reference',
+
+ inline: true,
+
+ group: 'inline',
+
+ atom: true,
+
+ addAttributes() {
+ return {
+ className: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ className: element.className,
+ };
+ },
+ },
+ referenceType: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ referenceType: element.dataset.referenceType,
+ };
+ },
+ },
+ originalText: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ originalText: element.dataset.original,
+ };
+ },
+ },
+ href: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ href: element.getAttribute('href'),
+ };
+ },
+ },
+ text: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ text: element.textContent,
+ };
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'a.gfm:not([data-link=true])',
+ priority: 51,
+ },
+ ];
+ },
+
+ renderHTML({ node }) {
+ return [
+ 'a',
+ {
+ class: node.attrs.className,
+ href: node.attrs.href,
+ 'data-reference-type': node.attrs.referenceType,
+ 'data-original': node.attrs.originalText,
+ },
+ node.attrs.text,
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/strike.js b/app/assets/javascripts/content_editor/extensions/strike.js
index 6f228e00994..b6c9a968fc2 100644
--- a/app/assets/javascripts/content_editor/extensions/strike.js
+++ b/app/assets/javascripts/content_editor/extensions/strike.js
@@ -1,9 +1 @@
-import { Strike } from '@tiptap/extension-strike';
-
-export const tiptapExtension = Strike;
-export const serializer = {
- open: '~~',
- close: '~~',
- mixable: true,
- expelEnclosingWhitespace: true,
-};
+export { Strike as default } from '@tiptap/extension-strike';
diff --git a/app/assets/javascripts/content_editor/extensions/subscript.js b/app/assets/javascripts/content_editor/extensions/subscript.js
new file mode 100644
index 00000000000..4bf89796efe
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/subscript.js
@@ -0,0 +1 @@
+export { Subscript as default } from '@tiptap/extension-subscript';
diff --git a/app/assets/javascripts/content_editor/extensions/superscript.js b/app/assets/javascripts/content_editor/extensions/superscript.js
new file mode 100644
index 00000000000..3eb7d86d90d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/superscript.js
@@ -0,0 +1 @@
+export { Superscript as default } from '@tiptap/extension-superscript';
diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js
index 566f7a21a85..0f0477cba2e 100644
--- a/app/assets/javascripts/content_editor/extensions/table.js
+++ b/app/assets/javascripts/content_editor/extensions/table.js
@@ -1,7 +1 @@
-import { Table } from '@tiptap/extension-table';
-
-export const tiptapExtension = Table;
-
-export function serializer(state, node) {
- state.renderContent(node);
-}
+export { Table as default } from '@tiptap/extension-table';
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index 6c25b867466..5bdc39231a1 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,9 +1,5 @@
import { TableCell } from '@tiptap/extension-table-cell';
-export const tiptapExtension = TableCell.extend({
+export default TableCell.extend({
content: 'inline*',
});
-
-export function serializer(state, node) {
- state.renderInline(node);
-}
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 3475857b9e6..23509706e4b 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,9 +1,5 @@
import { TableHeader } from '@tiptap/extension-table-header';
-export const tiptapExtension = TableHeader.extend({
+export default TableHeader.extend({
content: 'inline*',
});
-
-export function serializer(state, node) {
- state.renderInline(node);
-}
diff --git a/app/assets/javascripts/content_editor/extensions/table_row.js b/app/assets/javascripts/content_editor/extensions/table_row.js
index 07d2eb4faa2..541257a6cbf 100644
--- a/app/assets/javascripts/content_editor/extensions/table_row.js
+++ b/app/assets/javascripts/content_editor/extensions/table_row.js
@@ -1,51 +1,5 @@
import { TableRow } from '@tiptap/extension-table-row';
-export const tiptapExtension = TableRow.extend({
+export default TableRow.extend({
allowGapCursor: false,
});
-
-export function serializer(state, node) {
- const isHeaderRow = node.child(0).type.name === 'tableHeader';
-
- const renderRow = () => {
- const cellWidths = [];
-
- state.flushClose(1);
-
- state.write('| ');
- node.forEach((cell, _, i) => {
- if (i) state.write(' | ');
-
- const { length } = state.out;
- state.render(cell, node, i);
- cellWidths.push(state.out.length - length);
- });
- state.write(' |');
-
- state.closeBlock(node);
-
- return cellWidths;
- };
-
- const renderHeaderRow = (cellWidths) => {
- state.flushClose(1);
-
- state.write('|');
- node.forEach((cell, _, i) => {
- if (i) state.write('|');
-
- state.write(cell.attrs.align === 'center' ? ':' : '-');
- state.write(state.repeat('-', cellWidths[i]));
- state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
- });
- state.write('|');
-
- state.closeBlock(node);
- };
-
- if (isHeaderRow) {
- renderHeaderRow(renderRow());
- } else {
- renderRow();
- }
-}
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
new file mode 100644
index 00000000000..6163c0e043b
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -0,0 +1,33 @@
+import { TaskItem } from '@tiptap/extension-task-item';
+
+export default TaskItem.extend({
+ defaultOptions: {
+ nested: true,
+ HTMLAttributes: {},
+ },
+
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ parseHTML: (element) => {
+ const checkbox = element.querySelector('input[type=checkbox].task-list-item-checkbox');
+ return { checked: checkbox?.checked };
+ },
+ renderHTML: (attributes) => ({
+ 'data-checked': attributes.checked,
+ }),
+ keepOnSplit: false,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: 'li.task-list-item',
+ priority: 100,
+ },
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/task_list.js b/app/assets/javascripts/content_editor/extensions/task_list.js
new file mode 100644
index 00000000000..b7f6c857bc7
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/task_list.js
@@ -0,0 +1,30 @@
+import { mergeAttributes } from '@tiptap/core';
+import { TaskList } from '@tiptap/extension-task-list';
+
+export default TaskList.extend({
+ addAttributes() {
+ return {
+ type: {
+ default: 'ul',
+ parseHTML: (element) => {
+ return {
+ type: element.tagName.toLowerCase() === 'ol' ? 'ol' : 'ul',
+ };
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: '.task-list',
+ priority: 100,
+ },
+ ];
+ },
+
+ renderHTML({ HTMLAttributes: { type, ...HTMLAttributes } }) {
+ return [type, mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/text.js b/app/assets/javascripts/content_editor/extensions/text.js
index 0d76aa1f1a7..a2865e7010b 100644
--- a/app/assets/javascripts/content_editor/extensions/text.js
+++ b/app/assets/javascripts/content_editor/extensions/text.js
@@ -1,5 +1 @@
-import { Text } from '@tiptap/extension-text';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-
-export const tiptapExtension = Text;
-export const serializer = defaultMarkdownSerializer.nodes.text;
+export { Text as default } from '@tiptap/extension-text';
diff --git a/app/assets/javascripts/content_editor/services/build_serializer_config.js b/app/assets/javascripts/content_editor/services/build_serializer_config.js
deleted file mode 100644
index 75e2b0f9eba..00000000000
--- a/app/assets/javascripts/content_editor/services/build_serializer_config.js
+++ /dev/null
@@ -1,22 +0,0 @@
-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
index 29553f4c2ca..a387322bff7 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -1,8 +1,11 @@
+import eventHubFactory from '~/helpers/event_hub_factory';
+import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
+ this._eventHub = eventHubFactory();
}
get tiptapEditor() {
@@ -16,12 +19,45 @@ export class ContentEditor {
return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0);
}
+ dispose() {
+ this.tiptapEditor.destroy();
+ }
+
+ once(type, handler) {
+ this._eventHub.$once(type, handler);
+ }
+
+ on(type, handler) {
+ this._eventHub.$on(type, handler);
+ }
+
+ emit(type, params = {}) {
+ this._eventHub.$emit(type, params);
+ }
+
+ off(type, handler) {
+ this._eventHub.$off(type, handler);
+ }
+
+ disposeAllEvents() {
+ this._eventHub.dispose();
+ }
+
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer } = this;
- editor.commands.setContent(
- await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
- );
+ try {
+ this._eventHub.$emit(LOADING_CONTENT_EVENT);
+ const document = await serializer.deserialize({
+ schema: editor.schema,
+ content: serializedContent,
+ });
+ editor.commands.setContent(document);
+ this._eventHub.$emit(LOADING_SUCCESS_EVENT);
+ } catch (e) {
+ this._eventHub.$emit(LOADING_ERROR_EVENT, e);
+ throw e;
+ }
}
getSerializedContent() {
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 9251fdbbdc5..8997960203a 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -1,38 +1,43 @@
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 Strike from '../extensions/strike';
-import * as Table from '../extensions/table';
-import * as TableCell from '../extensions/table_cell';
-import * as TableHeader from '../extensions/table_header';
-import * as TableRow from '../extensions/table_row';
-import * as Text from '../extensions/text';
-import buildSerializerConfig from './build_serializer_config';
+import Attachment from '../extensions/attachment';
+import Blockquote from '../extensions/blockquote';
+import Bold from '../extensions/bold';
+import BulletList from '../extensions/bullet_list';
+import Code from '../extensions/code';
+import CodeBlockHighlight from '../extensions/code_block_highlight';
+import Document from '../extensions/document';
+import Dropcursor from '../extensions/dropcursor';
+import Emoji from '../extensions/emoji';
+import Gapcursor from '../extensions/gapcursor';
+import HardBreak from '../extensions/hard_break';
+import Heading from '../extensions/heading';
+import History from '../extensions/history';
+import HorizontalRule from '../extensions/horizontal_rule';
+import Image from '../extensions/image';
+import InlineDiff from '../extensions/inline_diff';
+import Italic from '../extensions/italic';
+import Link from '../extensions/link';
+import ListItem from '../extensions/list_item';
+import Loading from '../extensions/loading';
+import OrderedList from '../extensions/ordered_list';
+import Paragraph from '../extensions/paragraph';
+import Reference from '../extensions/reference';
+import Strike from '../extensions/strike';
+import Subscript from '../extensions/subscript';
+import Superscript from '../extensions/superscript';
+import Table from '../extensions/table';
+import TableCell from '../extensions/table_cell';
+import TableHeader from '../extensions/table_header';
+import TableRow from '../extensions/table_row';
+import TaskItem from '../extensions/task_item';
+import TaskList from '../extensions/task_list';
+import Text from '../extensions/text';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
-const collectTiptapExtensions = (extensions = []) =>
- extensions.map(({ tiptapExtension }) => tiptapExtension);
-
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
extensions: [...extensions],
@@ -48,6 +53,7 @@ export const createContentEditor = ({
renderMarkdown,
uploadsPath,
extensions = [],
+ serializerConfig = { marks: {}, nodes: {} },
tiptapOptions,
} = {}) => {
if (!isFunction(renderMarkdown)) {
@@ -55,6 +61,7 @@ export const createContentEditor = ({
}
const builtInContentEditorExtensions = [
+ Attachment.configure({ uploadsPath, renderMarkdown }),
Blockquote,
Bold,
BulletList,
@@ -62,29 +69,36 @@ export const createContentEditor = ({
CodeBlockHighlight,
Document,
Dropcursor,
+ Emoji,
Gapcursor,
HardBreak,
Heading,
History,
HorizontalRule,
- Image.configure({ uploadsPath, renderMarkdown }),
+ Image,
+ InlineDiff,
Italic,
Link,
ListItem,
+ Loading,
OrderedList,
Paragraph,
+ Reference,
Strike,
+ Subscript,
+ Superscript,
TableCell,
TableHeader,
TableRow,
Table,
+ TaskItem,
+ TaskList,
Text,
];
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
- const tiptapExtensions = collectTiptapExtensions(allExtensions).map(trackInputRulesAndShortcuts);
- const tiptapEditor = createTiptapEditor({ extensions: tiptapExtensions, ...tiptapOptions });
- const serializerConfig = buildSerializerConfig(allExtensions);
+ const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
+ const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
return new ContentEditor({ tiptapEditor, serializer });
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index f121cc9affd..df4d31c3d7f 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -1,5 +1,165 @@
-import { MarkdownSerializer as ProseMirrorMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+import {
+ MarkdownSerializer as ProseMirrorMarkdownSerializer,
+ defaultMarkdownSerializer,
+} from 'prosemirror-markdown/src/to_markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
+import Blockquote from '../extensions/blockquote';
+import Bold from '../extensions/bold';
+import BulletList from '../extensions/bullet_list';
+import Code from '../extensions/code';
+import CodeBlockHighlight from '../extensions/code_block_highlight';
+import Emoji from '../extensions/emoji';
+import HardBreak from '../extensions/hard_break';
+import Heading from '../extensions/heading';
+import HorizontalRule from '../extensions/horizontal_rule';
+import Image from '../extensions/image';
+import InlineDiff from '../extensions/inline_diff';
+import Italic from '../extensions/italic';
+import Link from '../extensions/link';
+import ListItem from '../extensions/list_item';
+import OrderedList from '../extensions/ordered_list';
+import Paragraph from '../extensions/paragraph';
+import Reference from '../extensions/reference';
+import Strike from '../extensions/strike';
+import Subscript from '../extensions/subscript';
+import Superscript from '../extensions/superscript';
+import Table from '../extensions/table';
+import TableCell from '../extensions/table_cell';
+import TableHeader from '../extensions/table_header';
+import TableRow from '../extensions/table_row';
+import TaskItem from '../extensions/task_item';
+import TaskList from '../extensions/task_list';
+import Text from '../extensions/text';
+
+const defaultSerializerConfig = {
+ marks: {
+ [Bold.name]: defaultMarkdownSerializer.marks.strong,
+ [Code.name]: defaultMarkdownSerializer.marks.code,
+ [Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
+ [Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
+ [Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
+ [InlineDiff.name]: {
+ mixable: true,
+ open(state, mark) {
+ return mark.attrs.type === 'addition' ? '{+' : '{-';
+ },
+ close(state, mark) {
+ return mark.attrs.type === 'addition' ? '+}' : '-}';
+ },
+ },
+ [Link.name]: {
+ open() {
+ return '[';
+ },
+ close(state, mark) {
+ const href = mark.attrs.canonicalSrc || mark.attrs.href;
+ return `](${state.esc(href)}${
+ mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''
+ })`;
+ },
+ },
+ [Strike.name]: {
+ open: '~~',
+ close: '~~',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+ },
+ },
+ nodes: {
+ [Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
+ [BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
+ [CodeBlockHighlight.name]: (state, node) => {
+ state.write(`\`\`\`${node.attrs.language || ''}\n`);
+ state.text(node.textContent, false);
+ state.ensureNewLine();
+ state.write('```');
+ state.closeBlock(node);
+ },
+ [Emoji.name]: (state, node) => {
+ const { name } = node.attrs;
+
+ state.write(`:${name}:`);
+ },
+ [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
+ [Heading.name]: defaultMarkdownSerializer.nodes.heading,
+ [HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
+ [Image.name]: (state, node) => {
+ const { alt, canonicalSrc, src, title } = node.attrs;
+ const quotedTitle = title ? ` ${state.quote(title)}` : '';
+
+ state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
+ },
+ [ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
+ [OrderedList.name]: defaultMarkdownSerializer.nodes.ordered_list,
+ [Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
+ [Reference.name]: (state, node) => {
+ state.write(node.attrs.originalText || node.attrs.text);
+ },
+ [Table.name]: (state, node) => {
+ state.renderContent(node);
+ },
+ [TableCell.name]: (state, node) => {
+ state.renderInline(node);
+ },
+ [TableHeader.name]: (state, node) => {
+ state.renderInline(node);
+ },
+ [TableRow.name]: (state, node) => {
+ const isHeaderRow = node.child(0).type.name === 'tableHeader';
+
+ const renderRow = () => {
+ const cellWidths = [];
+
+ state.flushClose(1);
+
+ state.write('| ');
+ node.forEach((cell, _, i) => {
+ if (i) state.write(' | ');
+
+ const { length } = state.out;
+ state.render(cell, node, i);
+ cellWidths.push(state.out.length - length);
+ });
+ state.write(' |');
+
+ state.closeBlock(node);
+
+ return cellWidths;
+ };
+
+ const renderHeaderRow = (cellWidths) => {
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === 'center' ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+ };
+
+ if (isHeaderRow) {
+ renderHeaderRow(renderRow());
+ } else {
+ renderRow();
+ }
+ },
+ [TaskItem.name]: (state, node) => {
+ state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
+ state.renderContent(node);
+ },
+ [TaskList.name]: (state, node) => {
+ if (node.attrs.type === 'ul') defaultMarkdownSerializer.nodes.bullet_list(state, node);
+ else defaultMarkdownSerializer.nodes.ordered_list(state, node);
+ },
+ [Text.name]: defaultMarkdownSerializer.nodes.text,
+ },
+};
const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
@@ -50,8 +210,16 @@ export default ({ render = () => null, serializerConfig }) => ({
*/
serialize: ({ schema, content }) => {
const proseMirrorDocument = schema.nodeFromJSON(content);
- const { nodes, marks } = serializerConfig;
- const serializer = new ProseMirrorMarkdownSerializer(nodes, marks);
+ const serializer = new ProseMirrorMarkdownSerializer(
+ {
+ ...defaultSerializerConfig.nodes,
+ ...serializerConfig.nodes,
+ },
+ {
+ ...defaultSerializerConfig.marks,
+ ...serializerConfig.marks,
+ },
+ );
return serializer.serialize(proseMirrorDocument, {
tightLists: true,
diff --git a/app/assets/javascripts/content_editor/services/track_ui_control.js b/app/assets/javascripts/content_editor/services/track_ui_control.js
new file mode 100644
index 00000000000..61f130ea861
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/track_ui_control.js
@@ -0,0 +1,9 @@
+import Tracking from '~/tracking';
+import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
+
+export default ({ action = TOOLBAR_CONTROL_TRACKING_ACTION, property, value } = {}) =>
+ Tracking.event(undefined, action, {
+ label: CONTENT_EDITOR_TRACKING_LABEL,
+ property,
+ value,
+ });
diff --git a/app/assets/javascripts/content_editor/services/upload_file.js b/app/assets/javascripts/content_editor/services/upload_file.js
deleted file mode 100644
index 613c53144a1..00000000000
--- a/app/assets/javascripts/content_editor/services/upload_file.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-const extractAttachmentLinkUrl = (html) => {
- const parser = new DOMParser();
- const { body } = parser.parseFromString(html, 'text/html');
- const link = body.querySelector('a');
- const src = link.getAttribute('href');
- const { canonicalSrc } = link.dataset;
-
- return { src, canonicalSrc };
-};
-
-/**
- * Uploads a file with a post request to the URL indicated
- * in the uploadsPath parameter. The expected response of the
- * uploads service is a JSON object that contains, at least, a
- * link property. The link property should contain markdown link
- * definition (i.e. [GitLab](https://gitlab.com)).
- *
- * This Markdown will be rendered to extract its canonical and full
- * URLs using GitLab Flavored Markdown renderer in the backend.
- *
- * @param {Object} params
- * @param {String} params.uploadsPath An absolute URL that points to a service
- * that allows sending a file for uploading via POST request.
- * @param {String} params.renderMarkdown A function that accepts a markdown string
- * and returns a rendered version in HTML format.
- * @param {File} params.file The file to upload
- *
- * @returns Returns an object with two properties:
- *
- * canonicalSrc: The URL as defined in the Markdown
- * src: The absolute URL that points to the resource in the server
- */
-export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
- const formData = new FormData();
- formData.append('file', file, file.name);
-
- const { data } = await axios.post(uploadsPath, formData);
- const { markdown } = data.link;
- const rendered = await renderMarkdown(markdown);
-
- return extractAttachmentLinkUrl(rendered);
-};
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
new file mode 100644
index 00000000000..8ac3f719309
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -0,0 +1,123 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import { extractFilename, readFileAsDataURL } from './utils';
+
+export const acceptedMimes = {
+ image: ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'],
+};
+
+const extractAttachmentLinkUrl = (html) => {
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+ const link = body.querySelector('a');
+ const src = link.getAttribute('href');
+ const { canonicalSrc } = link.dataset;
+
+ return { src, canonicalSrc };
+};
+
+/**
+ * Uploads a file with a post request to the URL indicated
+ * in the uploadsPath parameter. The expected response of the
+ * uploads service is a JSON object that contains, at least, a
+ * link property. The link property should contain markdown link
+ * definition (i.e. [GitLab](https://gitlab.com)).
+ *
+ * This Markdown will be rendered to extract its canonical and full
+ * URLs using GitLab Flavored Markdown renderer in the backend.
+ *
+ * @param {Object} params
+ * @param {String} params.uploadsPath An absolute URL that points to a service
+ * that allows sending a file for uploading via POST request.
+ * @param {String} params.renderMarkdown A function that accepts a markdown string
+ * and returns a rendered version in HTML format.
+ * @param {File} params.file The file to upload
+ *
+ * @returns Returns an object with two properties:
+ *
+ * canonicalSrc: The URL as defined in the Markdown
+ * src: The absolute URL that points to the resource in the server
+ */
+export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
+ const formData = new FormData();
+ formData.append('file', file, file.name);
+
+ const { data } = await axios.post(uploadsPath, formData);
+ const { markdown } = data.link;
+ const rendered = await renderMarkdown(markdown);
+
+ return extractAttachmentLinkUrl(rendered);
+};
+
+const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
+ const encodedSrc = await readFileAsDataURL(file);
+ const { view } = editor;
+
+ editor.commands.setImage({ uploading: true, src: encodedSrc });
+
+ const { state } = view;
+ const position = state.selection.from - 1;
+ const { tr } = state;
+
+ try {
+ const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
+
+ view.dispatch(
+ tr.setNodeMarkup(position, undefined, {
+ uploading: false,
+ src: encodedSrc,
+ alt: extractFilename(src),
+ canonicalSrc,
+ }),
+ );
+ } catch (e) {
+ editor.commands.deleteRange({ from: position, to: position + 1 });
+ editor.emit('error', {
+ error: __('An error occurred while uploading the image. Please try again.'),
+ });
+ }
+};
+
+const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) => {
+ await Promise.resolve();
+
+ const { view } = editor;
+
+ const text = extractFilename(file.name);
+
+ const { state } = view;
+ const { from } = state.selection;
+
+ editor.commands.insertContent({
+ type: 'loading',
+ attrs: { label: text },
+ });
+
+ try {
+ const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
+
+ editor.commands.insertContentAt(
+ { from, to: from + 1 },
+ { type: 'text', text, marks: [{ type: 'link', attrs: { href: src, canonicalSrc } }] },
+ );
+ } catch (e) {
+ editor.commands.deleteRange({ from, to: from + 1 });
+ editor.emit('error', {
+ error: __('An error occurred while uploading the file. Please try again.'),
+ });
+ }
+};
+
+export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
+ if (!file) return false;
+
+ if (acceptedMimes.image.includes(file?.type)) {
+ uploadImage({ editor, file, uploadsPath, renderMarkdown });
+
+ return true;
+ }
+
+ uploadAttachment({ editor, file, uploadsPath, renderMarkdown });
+
+ return true;
+};
diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js
index 2a2c7f617da..b3856b0dd74 100644
--- a/app/assets/javascripts/content_editor/services/utils.js
+++ b/app/assets/javascripts/content_editor/services/utils.js
@@ -4,8 +4,18 @@ export const hasSelection = (tiptapEditor) => {
return from < to;
};
-export const getImageAlt = (src) => {
- return src.replace(/^.*\/|\..*$/g, '').replace(/\W+/g, ' ');
+/**
+ * Extracts filename from a URL
+ *
+ * @example
+ * > extractFilename('https://gitlab.com/images/logo-full.png')
+ * < 'logo-full'
+ *
+ * @param {string} src The URL to extract filename from
+ * @returns {string}
+ */
+export const extractFilename = (src) => {
+ return src.replace(/^.*\/|\..+?$/g, '');
};
export const readFileAsDataURL = (file) => {
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index f104eb61e41..45c886978f1 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -78,7 +78,7 @@ export default {
return sprintf(
s__(
- 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}',
+ 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}',
),
{
startAwsLink:
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 8492f0b73e1..c9ecac6829b 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,16 +1,12 @@
<script>
-import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/cycle_analytics/components/stage_table.vue';
+import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { __ } from '~/locale';
-import banner from './banner.vue';
-import stageCodeComponent from './stage_code_component.vue';
-import stageComponent from './stage_component.vue';
-import stageNavItem from './stage_nav_item.vue';
-import stageReviewComponent from './stage_review_component.vue';
-import stageStagingComponent from './stage_staging_component.vue';
-import stageTestComponent from './stage_test_component.vue';
+import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
@@ -18,19 +14,11 @@ export default {
name: 'CycleAnalytics',
components: {
GlIcon,
- GlEmptyState,
GlLoadingIcon,
GlSprintf,
- banner,
- 'stage-issue-component': stageComponent,
- 'stage-plan-component': stageComponent,
- 'stage-code-component': stageCodeComponent,
- 'stage-test-component': stageTestComponent,
- 'stage-review-component': stageReviewComponent,
- 'stage-staging-component': stageStagingComponent,
- 'stage-production-component': stageComponent,
- 'stage-nav-item': stageNavItem,
PathNavigation,
+ StageTable,
+ ValueStreamMetrics,
},
props: {
noDataSvgPath: {
@@ -57,30 +45,56 @@ export default {
'selectedStageError',
'stages',
'summary',
- 'startDate',
+ 'daysInPast',
'permissions',
+ 'stageCounts',
+ 'endpoints',
+ 'features',
]),
- ...mapGetters(['pathNavigationData']),
+ ...mapGetters(['pathNavigationData', 'filterParams']),
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
},
displayNotEnoughData() {
- return this.selectedStageReady && this.isEmptyStage;
+ return !this.isLoadingStage && this.isEmptyStage;
},
displayNoAccess() {
- return this.selectedStageReady && !this.isUserAllowed(this.selectedStage.id);
+ return (
+ !this.isLoadingStage && this.selectedStage?.id && !this.isUserAllowed(this.selectedStage.id)
+ );
},
- selectedStageReady() {
- return !this.isLoadingStage && this.selectedStage;
+ displayPathNavigation() {
+ return this.isLoading || (this.selectedStage && this.pathNavigationData.length);
},
emptyStageTitle() {
+ if (this.displayNoAccess) {
+ return __('You need permission.');
+ }
return this.selectedStageError
? this.selectedStageError
: __("We don't have enough data to show this stage.");
},
emptyStageText() {
- return !this.selectedStageError ? this.selectedStage.emptyStageText : '';
+ if (this.displayNoAccess) {
+ return __('Want to see the data? Please ask an administrator for access.');
+ }
+ return !this.selectedStageError && this.selectedStage?.emptyStageText
+ ? this.selectedStage?.emptyStageText
+ : '';
+ },
+ selectedStageCount() {
+ if (this.selectedStage) {
+ const {
+ stageCounts,
+ selectedStage: { id },
+ } = this;
+ return stageCounts[id];
+ }
+ return 0;
+ },
+ metricsRequests() {
+ return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
},
},
methods: {
@@ -90,8 +104,8 @@ export default {
'setSelectedStage',
'setDateRange',
]),
- handleDateSelect(startDate) {
- this.setDateRange({ startDate });
+ handleDateSelect(daysInPast) {
+ this.setDateRange(daysInPast);
},
onSelectStage(stage) {
this.setSelectedStage(stage);
@@ -108,124 +122,62 @@ export default {
dayRangeOptions: [7, 30, 90],
i18n: {
dropdownText: __('Last %{days} days'),
+ pageTitle: __('Value Stream Analytics'),
+ recentActivity: __('Recent Project Activity'),
},
};
</script>
<template>
<div class="cycle-analytics">
- <path-navigation
- v-if="selectedStageReady"
- class="js-path-navigation gl-w-full gl-pb-2"
- :loading="isLoading"
- :stages="pathNavigationData"
- :selected-stage="selectedStage"
- :with-stage-counts="false"
- @selected="onSelectStage"
- />
- <gl-loading-icon v-if="isLoading" size="lg" />
- <div v-else class="wrapper">
- <!--
- We wont have access to the stage counts until we move to a default value stream
- For now we can use the `withStageCounts` flag to ensure we don't display empty stage counts
- Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/326705
- -->
- <div class="card" data-testid="vsa-stage-overview-metrics">
- <div class="card-header">{{ __('Recent Project Activity') }}</div>
- <div class="d-flex justify-content-between">
- <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">
- <template #days>{{ startDate }}</template>
- </gl-sprintf>
- <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
- </span>
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
- <a href="#" @click.prevent="handleDateSelect(days)">
- <gl-sprintf :message="$options.i18n.dropdownText">
- <template #days>{{ days }}</template>
- </gl-sprintf>
- </a>
- </li>
- </ul>
- </div>
- </div>
- </div>
- </div>
- <div class="stage-panel-container" data-testid="vsa-stage-table">
- <div class="card stage-panel gl-px-5">
- <div class="card-header border-bottom-0">
- <nav class="col-headers">
- <ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none">
- <li>
- <span v-if="selectedStage" class="stage-name font-weight-bold">{{
- selectedStage.legend ? __(selectedStage.legend) : __('Related Issues')
- }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="
- __('The collection of events added to the data gathered for that stage.')
- "
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- <li>
- <span class="stage-name font-weight-bold">{{ __('Time') }}</span>
- <span
- class="has-tooltip"
- data-placement="top"
- :title="__('The time taken by each data entry gathered by that stage.')"
- aria-hidden="true"
- >
- <gl-icon name="question-o" class="gl-text-gray-500" />
- </span>
- </li>
- </ul>
- </nav>
- </div>
- <div class="stage-panel-body">
- <section class="stage-events gl-overflow-auto gl-w-full">
- <gl-loading-icon v-if="isLoadingStage" size="lg" />
- <template v-else>
- <gl-empty-state
- v-if="displayNoAccess"
- class="js-empty-state"
- :title="__('You need permission.')"
- :svg-path="noAccessSvgPath"
- :description="__('Want to see the data? Please ask an administrator for access.')"
- />
- <template v-else>
- <gl-empty-state
- v-if="displayNotEnoughData"
- class="js-empty-state"
- :description="emptyStageText"
- :svg-path="noDataSvgPath"
- :title="emptyStageTitle"
- />
- <component
- :is="selectedStage.component"
- v-if="displayStageEvents"
- :stage="selectedStage"
- :items="selectedStageEvents"
- data-testid="stage-table-events"
- />
- </template>
- </template>
- </section>
- </div>
+ <h3>{{ $options.i18n.pageTitle }}</h3>
+ <div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
+ <path-navigation
+ v-if="displayPathNavigation"
+ class="js-path-navigation gl-w-full gl-pb-2"
+ :loading="isLoading || isLoadingStage"
+ :stages="pathNavigationData"
+ :selected-stage="selectedStage"
+ @selected="onSelectStage"
+ />
+ <div class="gl-flex-grow gl-align-self-end">
+ <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">
+ <template #days>{{ daysInPast }}</template>
+ </gl-sprintf>
+ <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
+ </span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
+ <a href="#" @click.prevent="handleDateSelect(days)">
+ <gl-sprintf :message="$options.i18n.dropdownText">
+ <template #days>{{ days }}</template>
+ </gl-sprintf>
+ </a>
+ </li>
+ </ul>
</div>
</div>
</div>
+ <value-stream-metrics
+ :request-path="endpoints.fullPath"
+ :request-params="filterParams"
+ :requests="metricsRequests"
+ />
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <stage-table
+ v-else
+ :is-loading="isLoading || isLoadingStage"
+ :stage-events="selectedStageEvents"
+ :selected-stage="selectedStage"
+ :stage-count="selectedStageCount"
+ :empty-state-title="emptyStageTitle"
+ :empty-state-message="emptyStageText"
+ :no-data-svg-path="noDataSvgPath"
+ :pagination="null"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
index 47fafc3b90c..f8f89772fd6 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -34,12 +34,7 @@ export default {
selectedStage: {
type: Object,
required: false,
- default: () => {},
- },
- withStageCounts: {
- type: Boolean,
- required: false,
- default: true,
+ default: () => ({}),
},
},
methods: {
@@ -81,7 +76,7 @@ export default {
<div class="gl-pb-4 gl-font-weight-bold">{{ pathItem.metric }}</div>
</div>
</div>
- <div v-if="withStageCounts" class="gl-px-4">
+ <div class="gl-px-4">
<div class="gl-display-flex gl-justify-content-space-between">
<div class="gl-pr-4 gl-pb-4">
{{ s__('ValueStreamEvent|Items in stage') }}
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue
deleted file mode 100644
index 6b757c6972a..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- userAvatarImage,
- limitWarning,
- totalTime,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- required: false,
- },
- stage: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(mergeRequest, i) in items" :key="i" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
- <h5 class="item-title merge-request-title">
- <a :href="mergeRequest.url"> {{ mergeRequest.title }} </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link"> !{{ mergeRequest.iid }} </a> &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="mergeRequest.url" class="issue-date"> {{ mergeRequest.createdAt }} </a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">
- {{ mergeRequest.author.name }}
- </a>
- </span>
- </div>
- <div class="item-time"><total-time :time="mergeRequest.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue
deleted file mode 100644
index cc7ae74dd3a..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- userAvatarImage,
- limitWarning,
- totalTime,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- required: false,
- },
- stage: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(issue, i) in items" :key="i" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="issue.author.avatarUrl" />
- <h5 class="item-title issue-title">
- <a :href="issue.url" class="issue-title"> {{ issue.title }} </a>
- </h5>
- <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a>
- </span>
- </div>
- <div class="item-time"><total-time :time="issue.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
deleted file mode 100644
index 4b15bd55cbd..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-export default {
- name: 'StageNavItem',
- props: {
- isDefaultStage: {
- type: Boolean,
- default: false,
- required: false,
- },
- isActive: {
- type: Boolean,
- default: false,
- required: false,
- },
- isUserAllowed: {
- type: Boolean,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- value: {
- type: String,
- default: '',
- required: false,
- },
- },
- computed: {
- hasValue() {
- return this.value && this.value.length > 0;
- },
- },
-};
-</script>
-
-<template>
- <li @click="$emit('select')">
- <div
- :class="{ active: isActive }"
- class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
- >
- <div
- class="stage-nav-item-cell stage-name w-50 pr-2"
- :class="{ 'font-weight-bold': isActive }"
- >
- {{ title }}
- </div>
- <div class="stage-nav-item-cell stage-median w-50">
- <template v-if="isUserAllowed">
- <span v-if="hasValue">{{ value }}</span>
- <span v-else class="stage-empty">{{ __('Not enough data') }}</span>
- </template>
- <template v-else>
- <span class="not-available">{{ __('Not available') }}</span>
- </template>
- </div>
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
deleted file mode 100644
index 33b4e649ab0..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- userAvatarImage,
- totalTime,
- limitWarning,
- GlIcon,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- required: false,
- },
- stage: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(mergeRequest, i) in items" :key="i" class="stage-event-item">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="mergeRequest.author.avatarUrl" />
- <h5 class="item-title merge-request-title">
- <a :href="mergeRequest.url"> {{ mergeRequest.title }} </a>
- </h5>
- <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> &middot;
- <span>
- {{ s__('OpenedNDaysAgo|Opened') }}
- <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
- </span>
- <span>
- {{ s__('ByAuthor|by') }}
- <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{
- mergeRequest.author.name
- }}</a>
- </span>
- <template v-if="mergeRequest.state === 'closed'">
- <span class="merge-request-state">
- <gl-icon name="cancel" class="gl-vertical-align-text-bottom" />
- {{ __('CLOSED') }}
- </span>
- </template>
- <template v-else>
- <span v-if="mergeRequest.branch" class="merge-request-branch">
- <gl-icon :size="16" name="fork" />
- <a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} </a>
- </span>
- </template>
- </div>
- <div class="item-time"><total-time :time="mergeRequest.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
deleted file mode 100644
index 6d8f711c13b..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- userAvatarImage,
- totalTime,
- limitWarning,
- GlIcon,
- },
- directives: {
- SafeHtml,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- required: false,
- },
- stage: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(build, i) in items" :key="i" class="stage-event-item item-build-component">
- <div class="item-details">
- <!-- FIXME: Pass an alt attribute here for accessibility -->
- <user-avatar-image :img-src="build.author.avatarUrl" />
- <h5 class="item-title">
- <a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <gl-icon :size="16" name="fork" />
- <a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
- <span class="icon-branch gl-text-gray-400">
- <gl-icon name="commit" :size="14" />
- </span>
- <a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
- </h5>
- <span>
- <a :href="build.url" class="build-date"> {{ build.date }} </a> {{ s__('ByAuthor|by') }}
- <a :href="build.author.webUrl" class="issue-author-link"> {{ build.author.name }} </a>
- </span>
- </div>
- <div class="item-time"><total-time :time="build.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
new file mode 100644
index 00000000000..0c47838c773
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -0,0 +1,266 @@
+<script>
+import {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
+ GlBadge,
+} from '@gitlab/ui';
+import FormattedStageCount from '~/cycle_analytics/components/formatted_stage_count.vue';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ NOT_ENOUGH_DATA_ERROR,
+ PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
+ PAGINATION_SORT_DIRECTION_ASC,
+ PAGINATION_SORT_DIRECTION_DESC,
+} from '../constants';
+import TotalTime from './total_time_component.vue';
+
+const DEFAULT_WORKFLOW_TITLE_PROPERTIES = {
+ thClass: 'gl-w-half',
+ key: PAGINATION_SORT_FIELD_END_EVENT,
+ sortable: true,
+};
+const WORKFLOW_COLUMN_TITLES = {
+ issues: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Issues') },
+ jobs: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Jobs') },
+ deployments: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Deployments') },
+ mergeRequests: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Merge requests') },
+};
+
+export default {
+ name: 'StageTable',
+ components: {
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
+ GlBadge,
+ TotalTime,
+ FormattedStageCount,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ selectedStage: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ stageEvents: {
+ type: Array,
+ required: true,
+ },
+ stageCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ noDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ emptyStateTitle: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ emptyStateMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ pagination: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ if (this.pagination) {
+ const {
+ pagination: { sort, direction },
+ } = this;
+ return {
+ sort,
+ direction,
+ sortDesc: direction === PAGINATION_SORT_DIRECTION_DESC,
+ };
+ }
+ return { sort: null, direction: null, sortDesc: null };
+ },
+ computed: {
+ isEmptyStage() {
+ return !this.selectedStage || !this.stageEvents.length;
+ },
+ emptyStateTitleText() {
+ return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
+ },
+ isMergeRequestStage() {
+ const [firstEvent] = this.stageEvents;
+ return this.isMrLink(firstEvent.url);
+ },
+ workflowTitle() {
+ if (this.isMergeRequestStage) {
+ return WORKFLOW_COLUMN_TITLES.mergeRequests;
+ }
+ return WORKFLOW_COLUMN_TITLES.issues;
+ },
+ fields() {
+ return [
+ this.workflowTitle,
+ {
+ key: PAGINATION_SORT_FIELD_DURATION,
+ label: __('Time'),
+ thClass: 'gl-w-half',
+ sortable: true,
+ },
+ ];
+ },
+ prevPage() {
+ return Math.max(this.pagination.page - 1, 0);
+ },
+ nextPage() {
+ return this.pagination.hasNextPage ? this.pagination.page + 1 : null;
+ },
+ },
+ methods: {
+ isMrLink(url = '') {
+ return url.includes('/merge_request');
+ },
+ itemId({ url, iid }) {
+ return this.isMrLink(url) ? `!${iid}` : `#${iid}`;
+ },
+ itemTitle(item) {
+ return item.title || item.name;
+ },
+ onSelectPage(page) {
+ const { sort, direction } = this.pagination;
+ this.track('click_button', { label: 'pagination' });
+ this.$emit('handleUpdatePagination', { sort, direction, page });
+ },
+ onSort({ sortBy, sortDesc }) {
+ const direction = sortDesc ? PAGINATION_SORT_DIRECTION_DESC : PAGINATION_SORT_DIRECTION_ASC;
+ this.sort = sortBy;
+ this.sortDesc = sortDesc;
+ this.$emit('handleUpdatePagination', { sort: sortBy, direction });
+ this.track('click_button', { label: `sort_${sortBy}_${direction}` });
+ },
+ },
+};
+</script>
+<template>
+ <div data-testid="vsa-stage-table">
+ <gl-loading-icon v-if="isLoading" class="gl-mt-4" size="md" />
+ <gl-empty-state
+ v-else-if="isEmptyStage"
+ :title="emptyStateTitleText"
+ :description="emptyStateMessage"
+ :svg-path="noDataSvgPath"
+ />
+ <gl-table
+ v-else
+ head-variant="white"
+ stacked="lg"
+ thead-class="border-bottom"
+ show-empty
+ :sort-by.sync="sort"
+ :sort-direction.sync="direction"
+ :sort-desc.sync="sortDesc"
+ :fields="fields"
+ :items="stageEvents"
+ :empty-text="emptyStateMessage"
+ @sort-changed="onSort"
+ >
+ <template v-if="stageCount" #head(end_event)="data">
+ <span>{{ data.label }}</span
+ ><gl-badge class="gl-ml-2" size="sm"
+ ><formatted-stage-count :stage-count="stageCount"
+ /></gl-badge>
+ </template>
+ <template #cell(end_event)="{ item }">
+ <div data-testid="vsa-stage-event">
+ <div v-if="item.id" data-testid="vsa-stage-content">
+ <p class="gl-m-0">
+ <gl-link class="gl-text-black-normal pipeline-id" :href="item.url"
+ >#{{ item.id }}</gl-link
+ >
+ <gl-icon :size="16" name="fork" />
+ <gl-link
+ v-if="item.branch"
+ :href="item.branch.url"
+ class="gl-text-black-normal ref-name"
+ >{{ item.branch.name }}</gl-link
+ >
+ <span class="icon-branch gl-text-gray-400">
+ <gl-icon name="commit" :size="14" />
+ </span>
+ <gl-link
+ class="commit-sha"
+ :href="item.commitUrl"
+ data-testid="vsa-stage-event-build-sha"
+ >{{ item.shortSha }}</gl-link
+ >
+ </p>
+ <p class="gl-m-0">
+ <span data-testid="vsa-stage-event-build-author-and-date">
+ <gl-link class="gl-text-black-normal build-date" :href="item.url">{{
+ item.date
+ }}</gl-link>
+ {{ s__('ByAuthor|by') }}
+ <gl-link
+ class="gl-text-black-normal issue-author-link"
+ :href="item.author.webUrl"
+ >{{ item.author.name }}</gl-link
+ >
+ </span>
+ </p>
+ </div>
+ <div v-else data-testid="vsa-stage-content">
+ <h5 class="gl-font-weight-bold gl-my-1" data-testid="vsa-stage-event-title">
+ <gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link>
+ </h5>
+ <p class="gl-m-0">
+ <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link>
+ <span class="gl-font-lg">&middot;</span>
+ <span data-testid="vsa-stage-event-date">
+ {{ s__('OpenedNDaysAgo|Opened') }}
+ <gl-link class="gl-text-black-normal" :href="item.url">{{
+ item.createdAt
+ }}</gl-link>
+ </span>
+ <span data-testid="vsa-stage-event-author">
+ {{ s__('ByAuthor|by') }}
+ <gl-link class="gl-text-black-normal" :href="item.author.webUrl">{{
+ item.author.name
+ }}</gl-link>
+ </span>
+ </p>
+ </div>
+ </div>
+ </template>
+ <template #cell(duration)="{ item }">
+ <total-time :time="item.totalTime" data-testid="vsa-stage-event-time" />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="pagination && !isLoading && !isEmptyStage"
+ :value="pagination.page"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-3"
+ data-testid="vsa-stage-pagination"
+ @input="onSelectPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
deleted file mode 100644
index c165c8cee78..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import limitWarning from './limit_warning_component.vue';
-import totalTime from './total_time_component.vue';
-
-export default {
- components: {
- totalTime,
- limitWarning,
- GlIcon,
- },
- props: {
- items: {
- type: Array,
- default: () => [],
- required: false,
- },
- stage: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="events-description">
- {{ stage.description }}
- <limit-warning :count="items.length" />
- </div>
- <ul class="stage-event-list">
- <li v-for="(build, i) in items" :key="i" class="stage-event-item item-build-component">
- <div class="item-details">
- <h5 class="item-title">
- <span class="icon-build-status gl-text-green-500">
- <gl-icon name="status_success" :size="14" />
- </span>
- <a :href="build.url" class="item-build-name"> {{ build.name }} </a> &middot;
- <a :href="build.url" class="pipeline-id"> #{{ build.id }} </a>
- <gl-icon :size="16" name="fork" />
- <a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a>
- <span class="icon-branch gl-text-gray-400">
- <gl-icon name="commit" :size="14" />
- </span>
- <a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a>
- </h5>
- <span>
- <a :href="build.url" class="issue-date"> {{ build.date }} </a>
- </span>
- </div>
- <div class="item-time"><total-time :time="build.totalTime" /></div>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
index f52438ca2cb..a5a90a56974 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue
@@ -1,4 +1,6 @@
<script>
+import { n__, s__ } from '~/locale';
+
export default {
props: {
time: {
@@ -11,24 +13,48 @@ export default {
hasData() {
return Object.keys(this.time).length;
},
+ calculatedTime() {
+ const {
+ time: { days = null, mins = null, hours = null, seconds = null },
+ } = this;
+
+ if (days) {
+ return {
+ duration: days,
+ units: n__('day', 'days', days),
+ };
+ }
+
+ if (hours) {
+ return {
+ duration: hours,
+ units: n__('Time|hr', 'Time|hrs', hours),
+ };
+ }
+
+ if (mins && !days) {
+ return {
+ duration: mins,
+ units: n__('Time|min', 'Time|mins', mins),
+ };
+ }
+
+ if ((seconds && this.hasData === 1) || seconds === 0) {
+ return {
+ duration: seconds,
+ units: s__('Time|s'),
+ };
+ }
+
+ return { duration: null, units: null };
+ },
},
};
</script>
<template>
<span class="total-time">
<template v-if="hasData">
- <template v-if="time.days">
- {{ time.days }} <span> {{ n__('day', 'days', time.days) }} </span>
- </template>
- <template v-if="time.hours">
- {{ time.hours }} <span> {{ n__('Time|hr', 'Time|hrs', time.hours) }} </span>
- </template>
- <template v-if="time.mins && !time.days">
- {{ time.mins }} <span> {{ n__('Time|min', 'Time|mins', time.mins) }} </span>
- </template>
- <template v-if="(time.seconds && hasData === 1) || time.seconds === 0">
- {{ time.seconds }} <span> {{ s__('Time|s') }} </span>
- </template>
+ {{ calculatedTime.duration }} <span>{{ calculatedTime.units }}</span>
</template>
<template v-else> -- </template>
</span>
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
new file mode 100644
index 00000000000..7371ffd2c7c
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
@@ -0,0 +1,107 @@
+<script>
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlPopover } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { flatten } from 'lodash';
+import createFlash from '~/flash';
+import { sprintf, s__ } from '~/locale';
+import { METRICS_POPOVER_CONTENT } from '../constants';
+import { removeFlash, prepareTimeMetricsData } from '../utils';
+
+const requestData = ({ request, endpoint, path, params, name }) => {
+ return request({ endpoint, params, requestPath: path })
+ .then(({ data }) => data)
+ .catch(() => {
+ const message = sprintf(
+ s__(
+ 'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
+ ),
+ { requestTypeName: name },
+ );
+ createFlash({ message });
+ });
+};
+
+const fetchMetricsData = (reqs = [], path, params) => {
+ const promises = reqs.map((r) => requestData({ ...r, path, params }));
+ return Promise.all(promises).then((responses) =>
+ prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT),
+ );
+};
+
+export default {
+ name: 'ValueStreamMetrics',
+ components: {
+ GlPopover,
+ GlSingleStat,
+ GlSkeletonLoading,
+ },
+ props: {
+ requestPath: {
+ type: String,
+ required: true,
+ },
+ requestParams: {
+ type: Object,
+ required: true,
+ },
+ requests: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ metrics: [],
+ isLoading: false,
+ };
+ },
+ watch: {
+ requestParams() {
+ this.fetchData();
+ },
+ },
+ mounted() {
+ this.fetchData();
+ },
+ methods: {
+ fetchData() {
+ removeFlash();
+ this.isLoading = true;
+ return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
+ .then((data) => {
+ this.metrics = data;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-flex-wrap" data-testid="vsa-time-metrics">
+ <div v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6">
+ <gl-skeleton-loading />
+ </div>
+ <template v-else>
+ <div v-for="metric in metrics" :key="metric.key" class="gl-my-6 gl-pr-9">
+ <gl-single-stat
+ :id="metric.key"
+ :value="`${metric.value}`"
+ :title="metric.label"
+ :unit="metric.unit || ''"
+ :should-animate="true"
+ :animation-decimal-places="1"
+ tabindex="0"
+ />
+ <gl-popover :target="metric.key" placement="bottom">
+ <template #title>
+ <span class="gl-display-block gl-text-left">{{ metric.label }}</span>
+ </template>
+ <span v-if="metric.description">{{ metric.description }}</span>
+ </gl-popover>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index 97f502326e5..c1be2ce7096 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -1,3 +1,10 @@
+import {
+ getValueStreamMetrics,
+ METRIC_TYPE_SUMMARY,
+ METRIC_TYPE_TIME_SUMMARY,
+} from '~/api/analytics_api';
+import { __, s__ } from '~/locale';
+
export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
@@ -7,3 +14,55 @@ export const DEFAULT_VALUE_STREAM = {
slug: 'default',
name: 'default',
};
+
+export const NOT_ENOUGH_DATA_ERROR = s__(
+ "ValueStreamAnalyticsStage|We don't have enough data to show this stage.",
+);
+
+export const PAGINATION_TYPE = 'keyset';
+export const PAGINATION_SORT_FIELD_END_EVENT = 'end_event';
+export const PAGINATION_SORT_FIELD_DURATION = 'duration';
+export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
+export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
+
+export const I18N_VSA_ERROR_STAGES = __(
+ 'There was an error fetching value stream analytics stages.',
+);
+export const I18N_VSA_ERROR_STAGE_MEDIAN = __('There was an error fetching median data for stages');
+export const I18N_VSA_ERROR_SELECTED_STAGE = __(
+ 'There was an error fetching data for the selected stage',
+);
+
+export const OVERVIEW_METRICS = {
+ TIME_SUMMARY: 'TIME_SUMMARY',
+ RECENT_ACTIVITY: 'RECENT_ACTIVITY',
+};
+
+export const METRICS_POPOVER_CONTENT = {
+ 'lead-time': {
+ description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
+ },
+ 'cycle-time': {
+ description: s__(
+ 'ValueStreamAnalytics|Median time from issue first merge request created to issue closed.',
+ ),
+ },
+ 'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
+ 'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') },
+ deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') },
+ 'deployment-frequency': {
+ description: s__('ValueStreamAnalytics|Average number of deployments to production per day.'),
+ },
+ commits: {
+ description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'),
+ },
+};
+
+export const SUMMARY_METRICS_REQUEST = [
+ { endpoint: METRIC_TYPE_SUMMARY, name: __('recent activity'), request: getValueStreamMetrics },
+];
+
+export const METRICS_REQUESTS = [
+ { endpoint: METRIC_TYPE_TIME_SUMMARY, name: __('time summary'), request: getValueStreamMetrics },
+ ...SUMMARY_METRICS_REQUEST,
+];
diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js b/app/assets/javascripts/cycle_analytics/default_event_objects.js
deleted file mode 100644
index 57f9019d2f8..00000000000
--- a/app/assets/javascripts/cycle_analytics/default_event_objects.js
+++ /dev/null
@@ -1,98 +0,0 @@
-export default {
- issue: {
- created_at: '',
- url: '',
- iid: '',
- title: '',
- total_time: {},
- author: {
- avatar_url: '',
- id: '',
- name: '',
- web_url: '',
- },
- },
- plan: {
- title: '',
- commit_url: '',
- short_sha: '',
- total_time: {},
- author: {
- name: '',
- id: '',
- avatar_url: '',
- web_url: '',
- },
- },
- code: {
- title: '',
- iid: '',
- created_at: '',
- url: '',
- total_time: {},
- author: {
- name: '',
- id: '',
- avatar_url: '',
- web_url: '',
- },
- },
- test: {
- name: '',
- id: '',
- date: '',
- url: '',
- short_sha: '',
- commit_url: '',
- total_time: {},
- branch: {
- name: '',
- url: '',
- },
- },
- review: {
- title: '',
- iid: '',
- created_at: '',
- url: '',
- state: '',
- total_time: {},
- author: {
- name: '',
- id: '',
- avatar_url: '',
- web_url: '',
- },
- },
- staging: {
- id: '',
- short_sha: '',
- date: '',
- url: '',
- commit_url: '',
- total_time: {},
- author: {
- name: '',
- id: '',
- avatar_url: '',
- web_url: '',
- },
- branch: {
- name: '',
- url: '',
- },
- },
- production: {
- title: '',
- created_at: '',
- url: '',
- iid: '',
- total_time: {},
- author: {
- name: '',
- id: '',
- avatar_url: '',
- web_url: '',
- },
- },
-};
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 615f96c3860..3827db4d9b2 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -20,11 +20,12 @@ export default () => {
store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10),
groupPath,
- requestPath,
- fullPath,
+ endpoints: {
+ requestPath,
+ fullPath,
+ },
features: {
- cycleAnalyticsForGroups:
- (groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
+ cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
},
});
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 955f0c7271e..a7a2c8ea9d3 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -1,25 +1,29 @@
import {
getProjectValueStreamStages,
getProjectValueStreams,
- getProjectValueStreamStageData,
getProjectValueStreamMetrics,
getValueStreamStageMedian,
+ getValueStreamStageRecords,
+ getValueStreamStageCounts,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { DEFAULT_DAYS_TO_DISPLAY, DEFAULT_VALUE_STREAM } from '../constants';
+import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
- return dispatch('fetchValueStreamStages');
+ return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
};
export const fetchValueStreamStages = ({ commit, state }) => {
- const { fullPath, selectedValueStream } = state;
+ const {
+ endpoints: { fullPath },
+ selectedValueStream: { id },
+ } = state;
commit(types.REQUEST_VALUE_STREAM_STAGES);
- return getProjectValueStreamStages(fullPath, selectedValueStream.id)
+ return getProjectValueStreamStages(fullPath, id)
.then(({ data }) => commit(types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS, data))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAM_STAGES_ERROR, status);
@@ -37,16 +41,11 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
export const fetchValueStreams = ({ commit, dispatch, state }) => {
const {
- fullPath,
- features: { cycleAnalyticsForGroups },
+ endpoints: { fullPath },
} = state;
commit(types.REQUEST_VALUE_STREAMS);
- const stageRequests = ['setSelectedStage'];
- if (cycleAnalyticsForGroups) {
- stageRequests.push('fetchStageMedians');
- }
-
+ const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
.then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
@@ -54,9 +53,10 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
-
export const fetchCycleAnalyticsData = ({
- state: { requestPath },
+ state: {
+ endpoints: { requestPath },
+ },
getters: { legacyFilterParams },
commit,
}) => {
@@ -72,18 +72,10 @@ export const fetchCycleAnalyticsData = ({
});
};
-export const fetchStageData = ({
- state: { requestPath, selectedStage },
- getters: { legacyFilterParams },
- commit,
-}) => {
+export const fetchStageData = ({ getters: { requestParams, filterParams }, commit }) => {
commit(types.REQUEST_STAGE_DATA);
- return getProjectValueStreamStageData({
- requestPath,
- stageId: selectedStage.id,
- params: legacyFilterParams,
- })
+ return getValueStreamStageRecords(requestParams, filterParams)
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) {
@@ -120,8 +112,37 @@ export const fetchStageMedians = ({
.then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
+ createFlash({ message: I18N_VSA_ERROR_STAGE_MEDIAN });
+ });
+};
+
+const getStageCounts = ({ stageId, vsaParams, filterParams = {} }) => {
+ return getValueStreamStageCounts({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
+ id: stageId,
+ ...data,
+ }));
+};
+
+export const fetchStageCountValues = ({
+ state: { stages },
+ getters: { requestParams: vsaParams, filterParams },
+ commit,
+}) => {
+ commit(types.REQUEST_STAGE_COUNTS);
+ return Promise.all(
+ stages.map(({ id: stageId }) =>
+ getStageCounts({
+ vsaParams,
+ stageId,
+ filterParams,
+ }),
+ ),
+ )
+ .then((data) => commit(types.RECEIVE_STAGE_COUNTS_SUCCESS, data))
+ .catch((error) => {
+ commit(types.RECEIVE_STAGE_COUNTS_ERROR, error);
createFlash({
- message: __('There was an error fetching median data for stages'),
+ message: __('There was an error fetching stage total counts'),
});
});
};
@@ -132,22 +153,32 @@ export const setSelectedStage = ({ dispatch, commit, state: { stages } }, select
return dispatch('fetchStageData');
};
-const refetchData = (dispatch, commit) => {
- commit(types.SET_LOADING, true);
+export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
+
+const refetchStageData = (dispatch) => {
return Promise.resolve()
- .then(() => dispatch('fetchValueStreams'))
- .then(() => dispatch('fetchCycleAnalyticsData'))
- .finally(() => commit(types.SET_LOADING, false));
+ .then(() => dispatch('setLoading', true))
+ .then(() =>
+ Promise.all([
+ dispatch('fetchCycleAnalyticsData'),
+ dispatch('fetchStageData'),
+ dispatch('fetchStageMedians'),
+ ]),
+ )
+ .finally(() => dispatch('setLoading', false));
};
-export const setFilters = ({ dispatch, commit }) => refetchData(dispatch, commit);
+export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
-export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => {
- commit(types.SET_DATE_RANGE, { startDate });
- return refetchData(dispatch, commit);
+export const setDateRange = ({ dispatch, commit }, daysInPast) => {
+ commit(types.SET_DATE_RANGE, daysInPast);
+ return refetchStageData(dispatch);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
- return refetchData(dispatch, commit);
+
+ return dispatch('setLoading', true)
+ .then(() => dispatch('fetchValueStreams'))
+ .finally(() => dispatch('setLoading', false));
};
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
index 66971ea8a2e..9faccabcaad 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -13,11 +13,11 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
export const requestParams = (state) => {
const {
- selectedStage: { id: stageId = null },
- groupPath: groupId,
+ endpoints: { fullPath },
selectedValueStream: { id: valueStreamId },
+ selectedStage: { id: stageId = null },
} = state;
- return { valueStreamId, groupId, stageId };
+ return { requestPath: fullPath, valueStreamId, stageId };
};
const dateRangeParams = ({ createdAfter, createdBefore }) => ({
@@ -25,15 +25,14 @@ const dateRangeParams = ({ createdAfter, createdBefore }) => ({
created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
});
-export const legacyFilterParams = ({ startDate }) => {
+export const legacyFilterParams = ({ daysInPast }) => {
return {
- 'cycle_analytics[start_date]': startDate,
+ 'cycle_analytics[start_date]': daysInPast,
};
};
-export const filterParams = ({ id, ...rest }) => {
+export const filterParams = (state) => {
return {
- project_ids: [id],
- ...dateRangeParams(rest),
+ ...dateRangeParams(state),
};
};
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
index 11ed62a4081..0d94aad2ca5 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
@@ -24,3 +24,7 @@ export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
+
+export const REQUEST_STAGE_COUNTS = 'REQUEST_STAGE_COUNTS';
+export const RECEIVE_STAGE_COUNTS_SUCCESS = 'RECEIVE_STAGE_COUNTS_SUCCESS';
+export const RECEIVE_STAGE_COUNTS_ERROR = 'RECEIVE_STAGE_COUNTS_ERROR';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index a8b7a607b66..e41de85c1fa 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,19 +1,11 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
-import {
- decorateData,
- decorateEvents,
- formatMedianValues,
- calculateFormattedDayInPast,
-} from '../utils';
+import { formatMedianValues, calculateFormattedDayInPast } from '../utils';
import * as types from './mutation_types';
export default {
- [types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) {
- state.requestPath = requestPath;
- state.fullPath = fullPath;
- state.groupPath = groupPath;
- state.id = projectId;
+ [types.INITIALIZE_VSA](state, { endpoints, features }) {
+ state.endpoints = endpoints;
const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
state.createdBefore = now;
state.createdAfter = past;
@@ -28,9 +20,9 @@ export default {
[types.SET_SELECTED_STAGE](state, stage) {
state.selectedStage = stage;
},
- [types.SET_DATE_RANGE](state, { startDate }) {
- state.startDate = startDate;
- const { now, past } = calculateFormattedDayInPast(startDate);
+ [types.SET_DATE_RANGE](state, daysInPast) {
+ state.daysInPast = daysInPast;
+ const { now, past } = calculateFormattedDayInPast(daysInPast);
state.createdBefore = now;
state.createdAfter = past;
},
@@ -47,13 +39,7 @@ export default {
state.stages = [];
},
[types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS](state, { stages = [] }) {
- state.stages = stages.map((s) => ({
- ...convertObjectPropsToCamelCase(s, { deep: true }),
- // NOTE: we set the component type here to match the current behaviour
- // this can be removed when we migrate to the update stage table
- // https://gitlab.com/gitlab-org/gitlab/-/issues/326704
- component: `stage-${s.id}-component`,
- }));
+ state.stages = stages.map((s) => convertObjectPropsToCamelCase(s, { deep: true }));
},
[types.RECEIVE_VALUE_STREAM_STAGES_ERROR](state) {
state.stages = [];
@@ -61,25 +47,14 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.hasError = false;
- if (!state.features.cycleAnalyticsForGroups) {
- state.medians = {};
- }
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
- const { summary, medians } = decorateData(data);
- if (!state.features.cycleAnalyticsForGroups) {
- state.medians = formatMedianValues(medians);
- }
- state.permissions = data.permissions;
- state.summary = summary;
+ state.permissions = data?.permissions || {};
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.hasError = true;
- if (!state.features.cycleAnalyticsForGroups) {
- state.medians = {};
- }
},
[types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true;
@@ -87,11 +62,12 @@ export default {
state.selectedStageEvents = [];
state.hasError = false;
},
- [types.RECEIVE_STAGE_DATA_SUCCESS](state, { events = [] }) {
- const { selectedStage } = state;
+ [types.RECEIVE_STAGE_DATA_SUCCESS](state, events = []) {
state.isLoadingStage = false;
state.isEmptyStage = !events.length;
- state.selectedStageEvents = decorateEvents(events, selectedStage);
+ state.selectedStageEvents = events.map((ev) =>
+ convertObjectPropsToCamelCase(ev, { deep: true }),
+ );
state.hasError = false;
},
[types.RECEIVE_STAGE_DATA_ERROR](state, error) {
@@ -110,4 +86,19 @@ export default {
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {};
},
+ [types.REQUEST_STAGE_COUNTS](state) {
+ state.stageCounts = {};
+ },
+ [types.RECEIVE_STAGE_COUNTS_SUCCESS](state, stageCounts = []) {
+ state.stageCounts = stageCounts.reduce(
+ (acc, { id, count }) => ({
+ ...acc,
+ [id]: count,
+ }),
+ {},
+ );
+ },
+ [types.RECEIVE_STAGE_COUNTS_ERROR](state) {
+ state.stageCounts = {};
+ },
};
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
index 4d61077fb99..e6da3f609b2 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -1,11 +1,10 @@
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
- features: {},
id: null,
- requestPath: '',
- fullPath: '',
- startDate: DEFAULT_DAYS_TO_DISPLAY,
+ features: {},
+ endpoints: {},
+ daysInPast: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null,
createdBefore: null,
stages: [],
@@ -18,10 +17,10 @@ export default () => ({
selectedStageEvents: [],
selectedStageError: '',
medians: {},
+ stageCounts: {},
hasError: false,
isLoading: false,
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
- parentPath: null,
});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index a1690dd1513..fa02fdf914a 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,38 +1,19 @@
import dateFormat from 'dateformat';
import { unescape } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants';
+import { hideFlash } from '~/flash';
import { sanitize } from '~/lib/dompurify';
-import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
+import { slugify } from '~/lib/utils/text_utility';
import { s__, sprintf } from '../locale';
-import DEFAULT_EVENT_OBJECTS from './default_event_objects';
-/**
- * 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 mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
-const mapToMedians = ({ name: id, value }) => ({ id, value });
-
-export const decorateData = (data = {}) => {
- const { stats: stages, summary } = data;
- return {
- summary: summary?.map((item) => mapToSummary(item)) || [],
- medians: stages?.map((item) => mapToMedians(item)) || [],
- };
+export const removeFlash = (type = 'alert') => {
+ const flashEl = document.querySelector(`.flash-${type}`);
+ if (flashEl) {
+ hideFlash(flashEl);
+ }
};
/**
@@ -135,3 +116,36 @@ export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
past: toIsoFormat(getDateInPast(today, daysInPast)),
};
};
+
+/**
+ * @typedef {Object} MetricData
+ * @property {String} title - Title of the metric measured
+ * @property {String} value - String representing the decimal point value, e.g '1.5'
+ * @property {String} [unit] - String representing the decimal point value, e.g '1.5'
+ *
+ * @typedef {Object} TransformedMetricData
+ * @property {String} label - Title of the metric measured
+ * @property {String} value - String representing the decimal point value, e.g '1.5'
+ * @property {String} key - Slugified string based on the 'title'
+ * @property {String} description - String to display for a description
+ * @property {String} unit - String representing the decimal point value, e.g '1.5'
+ */
+
+/**
+ * Prepares metric data to be rendered in the metric_card component
+ *
+ * @param {MetricData[]} data - The metric data to be rendered
+ * @param {Object} popoverContent - Key value pair of data to display in the popover
+ * @returns {TransformedMetricData[]} An array of metrics ready to render in the metric_card
+ */
+
+export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
+ data.map(({ title: label, ...rest }) => {
+ const key = slugify(label);
+ return {
+ ...rest,
+ label,
+ key,
+ description: popoverContent[key]?.description || '',
+ };
+ });
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index a1dd12ff769..69f1d62539a 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -281,6 +281,7 @@ export class GitLabDropdown {
$target &&
!$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.is('use') &&
!$target.data('isLink')
) {
e.stopPropagation();
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 78ba586ce37..813f87452d8 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -4,13 +4,16 @@ import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
+import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../../mixins/all_versions';
import { hasErrors } from '../../utils/cache_update';
+import { extractDesign } from '../../utils/design_management_utils';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue';
import DesignReplyForm from './design_reply_form.vue';
@@ -161,6 +164,19 @@ export default {
},
toggleResolvedStatus() {
this.isResolving = true;
+
+ /**
+ * Get previous todo count
+ */
+ const { defaultClient: client } = this.$apollo.provider.clients;
+ const sourceData = client.readQuery({
+ query: getDesignQuery,
+ variables: this.designVariables,
+ });
+
+ const design = extractDesign(sourceData);
+ const prevTodoCount = design.currentUserTodos?.nodes?.length || 0;
+
this.$apollo
.mutate({
mutation: toggleResolveDiscussionMutation,
@@ -170,6 +186,10 @@ export default {
if (data.errors?.length > 0) {
this.$emit('resolve-discussion-error', data.errors[0]);
}
+ const newTodoCount =
+ data?.discussionToggleResolve?.discussion?.noteable?.currentUserTodos?.nodes?.length ||
+ 0;
+ updateGlobalTodoCount(newTodoCount - prevTodoCount);
})
.catch((err) => {
this.$emit('resolve-discussion-error', err);
diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue
index e64ee4a5a34..8ab94cd2c4b 100644
--- a/app/assets/javascripts/design_management/components/image.vue
+++ b/app/assets/javascripts/design_management/components/image.vue
@@ -1,6 +1,8 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
+import { DESIGN_MARK_APP_START, DESIGN_MAIN_IMAGE_OUTPUT } from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
export default {
components: {
@@ -39,7 +41,9 @@ export default {
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
- this.onImgLoad();
+ if (!this.image) {
+ this.onImgLoad();
+ }
this.resizeThrottled = throttle(() => {
// NOTE: if imageStyle is set, then baseImageSize
@@ -53,6 +57,14 @@ export default {
methods: {
onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
+ performanceMarkAndMeasure({
+ measures: [
+ {
+ name: DESIGN_MAIN_IMAGE_OUTPUT,
+ start: DESIGN_MARK_APP_START,
+ },
+ ],
+ });
},
onImgError() {
this.imageError = true;
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index 750f16bbe57..816d7ac7abf 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,6 +1,8 @@
<script>
import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import defaultAvatarUrl from 'images/no_avatar.png';
import { __, sprintf } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
@@ -9,6 +11,7 @@ export default {
GlDropdown,
GlDropdownItem,
GlSprintf,
+ TimeAgo,
},
mixins: [allVersionsMixin],
computed: {
@@ -58,6 +61,9 @@ export default {
}
return __('Version %{versionNumber}');
},
+ getAvatarUrl(version) {
+ return version?.author?.avatarUrl || defaultAvatarUrl;
+ },
},
};
</script>
@@ -68,14 +74,28 @@ export default {
v-for="(version, index) in allVersions"
:key="version.id"
:is-check-item="true"
+ :is-check-centered="true"
:is-checked="findVersionId(version.id) === currentVersionId"
+ :avatar-url="getAvatarUrl(version)"
@click="routeToVersion(version.id)"
>
- <gl-sprintf :message="versionText(version.id)">
- <template #versionNumber>
- {{ allVersions.length - index }}
- </template>
- </gl-sprintf>
+ <strong>
+ <gl-sprintf :message="versionText(version.id)">
+ <template #versionNumber>
+ {{ allVersions.length - index }}
+ </template>
+ </gl-sprintf>
+ </strong>
+
+ <div v-if="version.author" class="gl-text-gray-600 gl-mt-1">
+ <div>{{ version.author.name }}</div>
+ <time-ago
+ v-if="version.createdAt"
+ class="text-1"
+ :time="version.createdAt"
+ tooltip-placement="bottom"
+ />
+ </div>
</gl-dropdown-item>
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
index 9a0547ee9db..fa57537f74e 100644
--- a/app/assets/javascripts/design_management/graphql.js
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -1,10 +1,11 @@
-import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import { defaultDataIdFromObject, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import produce from 'immer';
import { uniqueId } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
+import introspectionQueryResultData from './graphql/fragmentTypes.json';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
import getDesignQuery from './graphql/queries/get_design.query.graphql';
import typeDefs from './graphql/typedefs.graphql';
@@ -12,6 +13,10 @@ import { addPendingTodoToStore } from './utils/cache_update';
import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils';
import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages';
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
Vue.use(VueApollo);
const resolvers = {
@@ -80,6 +85,7 @@ const defaultClient = createDefaultClient(
}
return defaultDataIdFromObject(object);
},
+ fragmentMatcher,
},
typeDefs,
assumeImmutableResults: true,
diff --git a/app/assets/javascripts/design_management/graphql/fragmentTypes.json b/app/assets/javascripts/design_management/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..0953231ea4c
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"INTERFACE","name":"User","possibleTypes":[{"name":"UserCore"}]},{"kind":"UNION","name":"NoteableType","possibleTypes":[{"name":"Design"},{"name":"Issue"},{"name":"MergeRequest"}]}]}}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_todo_item.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_todo_item.fragment.graphql
new file mode 100644
index 00000000000..3fe20705ce2
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_todo_item.fragment.graphql
@@ -0,0 +1,11 @@
+fragment DesignTodoItem on Design {
+ id
+ image
+ __typename
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ __typename
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
index 0b8400ac040..41c3f56f477 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
@@ -1,4 +1,5 @@
#import "../fragments/design_note.fragment.graphql"
+#import "../fragments/design_todo_item.fragment.graphql"
mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
createImageDiffNote(input: $input) {
@@ -7,6 +8,11 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
discussion {
id
replyId
+ noteable {
+ ... on Design {
+ ...DesignTodoItem
+ }
+ }
notes {
nodes {
...DesignNote
diff --git a/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
index 1157fc05d5f..124f12ef018 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql
@@ -1,11 +1,17 @@
#import "../fragments/design_note.fragment.graphql"
#import "../fragments/discussion_resolved_status.fragment.graphql"
+#import "../fragments/design_todo_item.fragment.graphql"
mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) {
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
discussion {
id
...ResolvedStatus
+ noteable {
+ ... on Design {
+ ...DesignTodoItem
+ }
+ }
notes {
nodes {
...DesignNote
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index aa9f377ef16..11666587265 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import { DESIGN_MARK_APP_START, DESIGN_MEASURE_BEFORE_APP } from '~/performance/constants';
+import { performanceMarkAndMeasure } from '~/performance/utils';
import App from './components/app.vue';
import apolloProvider from './graphql';
import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
@@ -28,6 +30,16 @@ export default () => {
projectPath,
issueIid,
},
+ mounted() {
+ performanceMarkAndMeasure({
+ mark: DESIGN_MARK_APP_START,
+ measures: [
+ {
+ name: DESIGN_MEASURE_BEFORE_APP,
+ },
+ ],
+ });
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 19bfa123487..48ee7068809 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -1,10 +1,12 @@
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
+import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
@@ -93,6 +95,7 @@ export default {
errorMessage: '',
scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false,
+ prevCurrentUserTodos: null,
};
},
apollo: {
@@ -163,6 +166,13 @@ export default {
resolvedDiscussions() {
return this.discussions.filter((discussion) => discussion.resolved);
},
+ currentUserTodos() {
+ if (!this.design || !this.design.currentUserTodos) {
+ return null;
+ }
+
+ return this.design.currentUserTodos?.nodes?.length;
+ },
},
watch: {
resolvedDiscussions(val) {
@@ -170,6 +180,9 @@ export default {
this.resolvedDiscussionsExpanded = false;
}
},
+ currentUserTodos(_, prevCurrentUserTodos) {
+ this.prevCurrentUserTodos = prevCurrentUserTodos;
+ },
},
mounted() {
Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign);
@@ -272,9 +285,14 @@ export default {
this.$refs.newDiscussionForm.focusInput();
}
},
- closeCommentForm() {
+ closeCommentForm(data) {
this.comment = '';
this.annotationCoordinates = null;
+
+ if (data?.data && !isNull(this.prevCurrentUserTodos)) {
+ updateGlobalTodoCount(this.currentUserTodos - this.prevCurrentUserTodos);
+ this.prevCurrentUserTodos = this.currentUserTodos;
+ }
},
closeDesign() {
this.$router.push({
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index e33b60f8ab5..d03b5cbc26b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -14,9 +14,11 @@ import {
} from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import MrWidgetHowToMergeModal from '~/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import notesEventHub from '../../notes/event_hub';
@@ -46,12 +48,12 @@ import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
import { fileByFile } from '../utils/preferences';
+import { queueRedisHllEvents } from '../utils/queue_events';
import CollapsedFilesWarning from './collapsed_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
-import MergeConflictWarning from './merge_conflict_warning.vue';
import NoChanges from './no_changes.vue';
import PreRenderer from './pre_renderer.vue';
import TreeList from './tree_list.vue';
@@ -64,7 +66,6 @@ export default {
DiffFile,
NoChanges,
HiddenFilesWarning,
- MergeConflictWarning,
CollapsedFilesWarning,
CommitWidget,
TreeList,
@@ -76,6 +77,7 @@ export default {
DynamicScrollerItem,
PreRenderer,
VirtualScrollerScrollSync,
+ MrWidgetHowToMergeModal,
},
alerts: {
ALERT_OVERFLOW_HIDDEN,
@@ -163,6 +165,21 @@ export default {
required: false,
default: () => ({}),
},
+ sourceProjectDefaultUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourceProjectFullPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isForked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const treeWidth =
@@ -172,7 +189,6 @@ export default {
treeWidth,
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
- disableVirtualScroller: false,
};
},
computed: {
@@ -203,6 +219,8 @@ export default {
'mrReviews',
'renderTreeList',
'showWhitespace',
+ 'targetBranchName',
+ 'branchName',
]),
...mapGetters('diffs', [
'whichCollapsedTypes',
@@ -337,29 +355,33 @@ export default {
}
if (window.gon?.features?.diffSettingsUsageData) {
+ const events = [];
+
if (this.renderTreeList) {
- api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
+ events.push(TRACKING_FILE_BROWSER_TREE);
} else {
- api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_LIST);
+ events.push(TRACKING_FILE_BROWSER_LIST);
}
if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
- api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_INLINE);
+ events.push(TRACKING_DIFF_VIEW_INLINE);
} else {
- api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_PARALLEL);
+ events.push(TRACKING_DIFF_VIEW_PARALLEL);
}
if (this.showWhitespace) {
- api.trackRedisHllUserEvent(TRACKING_WHITESPACE_SHOW);
+ events.push(TRACKING_WHITESPACE_SHOW);
} else {
- api.trackRedisHllUserEvent(TRACKING_WHITESPACE_HIDE);
+ events.push(TRACKING_WHITESPACE_HIDE);
}
if (this.viewDiffsFileByFile) {
- api.trackRedisHllUserEvent(TRACKING_SINGLE_FILE_MODE);
+ events.push(TRACKING_SINGLE_FILE_MODE);
} else {
- api.trackRedisHllUserEvent(TRACKING_MULTIPLE_FILES_MODE);
+ events.push(TRACKING_MULTIPLE_FILES_MODE);
}
+
+ queueRedisHllEvents(events);
}
},
beforeCreate() {
@@ -414,6 +436,7 @@ export default {
'setShowTreeList',
'navigateToDiffFileIndex',
'setFileByFile',
+ 'disableVirtualScroller',
]),
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
@@ -506,9 +529,32 @@ export default {
);
}
- Mousetrap.bind(['ctrl+f', 'command+f'], () => {
- this.disableVirtualScroller = true;
- });
+ if (
+ window.gon?.features?.diffsVirtualScrolling ||
+ window.gon?.features?.diffSearchingUsageData
+ ) {
+ let keydownTime;
+ Mousetrap.bind(['mod+f', 'mod+g'], () => {
+ keydownTime = new Date().getTime();
+ });
+
+ window.addEventListener('blur', () => {
+ if (keydownTime) {
+ const delta = new Date().getTime() - keydownTime;
+
+ // To make sure the user is using the find function we need to wait for blur
+ // and max 1000ms to be sure it the search box is filtered
+ if (delta >= 0 && delta < 1000) {
+ this.disableVirtualScroller();
+
+ if (window.gon?.features?.diffSearchingUsageData) {
+ api.trackRedisHllUserEvent('i_code_review_user_searches_diff');
+ api.trackRedisCounterEvent('diff_searches');
+ }
+ }
+ }
+ });
+ }
},
removeEventListeners() {
Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
@@ -568,6 +614,9 @@ export default {
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
+ howToMergeDocsPath: helpPagePath('user/project/merge_requests/reviews/index.md', {
+ anchor: 'checkout-merge-requests-locally-through-the-head-ref',
+ }),
};
</script>
@@ -587,12 +636,6 @@ export default {
:plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath"
/>
- <merge-conflict-warning
- v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT"
- :limited="isLimitedContainer"
- :resolution-path="conflictResolutionPath"
- :mergeable="canMerge"
- />
<collapsed-files-warning
v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
:limited="isLimitedContainer"
@@ -628,7 +671,7 @@ export default {
<div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
<template v-else-if="renderDiffFiles">
<dynamic-scroller
- v-if="!disableVirtualScroller && isVirtualScrollingEnabled"
+ v-if="isVirtualScrollingEnabled"
ref="virtualScroller"
:items="diffs"
:min-item-size="70"
@@ -705,6 +748,15 @@ export default {
<no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
</div>
</div>
+ <mr-widget-how-to-merge-modal
+ :is-fork="isForked"
+ :can-merge="canMerge"
+ :source-branch="branchName"
+ :source-project-path="sourceProjectFullPath"
+ :target-branch="targetBranchName"
+ :source-project-default-url="sourceProjectDefaultUrl"
+ :reviewing-docs-path="$options.howToMergeDocsPath"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index dde5ea81e9a..933891d698c 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, GlSprintf } from '@gitlab/ui';
+import {
+ GlButton,
+ GlLoadingIcon,
+ GlSafeHtmlDirective as SafeHtml,
+ GlSprintf,
+ GlAlert,
+ GlModalDirective,
+} from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
@@ -19,7 +26,7 @@ import {
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
} from '../constants';
import eventHub from '../event_hub';
-import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
+import { DIFF_FILE, GENERIC_ERROR, CONFLICT_TEXT } from '../i18n';
import { collapsedType, getShortShaFromFile } from '../utils/diff_file';
import DiffContent from './diff_content.vue';
import DiffFileHeader from './diff_file_header.vue';
@@ -31,9 +38,11 @@ export default {
GlButton,
GlLoadingIcon,
GlSprintf,
+ GlAlert,
},
directives: {
SafeHtml,
+ GlModalDirective,
},
mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.file.file_hash })],
props: {
@@ -83,6 +92,7 @@ export default {
idState() {
return {
isLoadingCollapsedDiff: false,
+ hasLoadedCollapsedDiff: false,
forkMessageVisible: false,
hasToggled: false,
};
@@ -92,7 +102,12 @@ export default {
genericError: GENERIC_ERROR,
},
computed: {
- ...mapState('diffs', ['currentDiffFileId', 'codequalityDiff']),
+ ...mapState('diffs', [
+ 'currentDiffFileId',
+ 'codequalityDiff',
+ 'conflictResolutionPath',
+ 'canMerge',
+ ]),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled']),
viewBlobHref() {
@@ -181,7 +196,13 @@ export default {
},
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
- if (newHash && oldHash && !this.hasDiff && !this.preRender) {
+ if (
+ newHash &&
+ oldHash &&
+ !this.hasDiff &&
+ !this.preRender &&
+ !this.idState.hasLoadedCollapsedDiff
+ ) {
this.requestDiff();
}
},
@@ -198,6 +219,8 @@ export default {
if (this.hasDiff) {
this.postRender();
+ } else if (this.viewDiffsFileByFile && !this.isCollapsed) {
+ this.requestDiff();
}
this.manageViewedEffects();
@@ -265,14 +288,22 @@ export default {
}
},
requestDiff() {
- this.idState.isLoadingCollapsedDiff = true;
+ const { idState, file } = this;
- this.loadCollapsedDiff(this.file)
+ idState.isLoadingCollapsedDiff = true;
+
+ this.loadCollapsedDiff(file)
.then(() => {
- this.idState.isLoadingCollapsedDiff = false;
- this.setRenderIt(this.file);
+ idState.isLoadingCollapsedDiff = false;
+ idState.hasLoadedCollapsedDiff = true;
+
+ if (this.file.file_hash === file.file_hash) {
+ this.setRenderIt(this.file);
+ }
})
.then(() => {
+ if (this.file.file_hash !== file.file_hash) return;
+
requestIdleCallback(
() => {
this.postRender();
@@ -282,7 +313,7 @@ export default {
);
})
.catch(() => {
- this.idState.isLoadingCollapsedDiff = false;
+ idState.isLoadingCollapsedDiff = false;
createFlash({
message: this.$options.i18n.genericError,
});
@@ -295,6 +326,7 @@ export default {
this.idState.forkMessageVisible = false;
},
},
+ CONFLICT_TEXT,
};
</script>
@@ -373,6 +405,55 @@ export default {
<div v-else v-safe-html="errorMessage" class="nothing-here-block"></div>
</div>
<template v-else>
+ <gl-alert
+ v-if="file.conflict_type"
+ variant="danger"
+ :dismissible="false"
+ data-testid="conflictsAlert"
+ >
+ {{ $options.CONFLICT_TEXT[file.conflict_type] }}
+ <template v-if="!canMerge">
+ {{ __('Ask someone with write access to resolve it.') }}
+ </template>
+ <gl-sprintf
+ v-else-if="conflictResolutionPath"
+ :message="
+ __(
+ 'You can %{gitlabLinkStart}resolve conflicts on GitLab%{gitlabLinkEnd} or %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.',
+ )
+ "
+ >
+ <template #gitlabLink="{ content }">
+ <gl-button
+ :href="conflictResolutionPath"
+ variant="link"
+ class="gl-vertical-align-text-bottom"
+ >{{ content }}</gl-button
+ >
+ </template>
+ <template #resolveLocally="{ content }">
+ <gl-button
+ v-gl-modal-directive="'modal-merge-info'"
+ variant="link"
+ class="gl-vertical-align-text-bottom"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ <gl-sprintf
+ v-else
+ :message="__('You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.')"
+ >
+ <template #resolveLocally="{ content }">
+ <gl-button
+ v-gl-modal-directive="'modal-merge-info'"
+ variant="link"
+ class="gl-vertical-align-text-bottom"
+ >{{ content }}</gl-button
+ >
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<div
v-if="showWarning"
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 1f3ec7092bc..e2f3f9cad7b 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -75,6 +75,7 @@ export default {
:key="note.id"
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
+ lazy
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="$emit('toggleLineDiscussions')"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index c310bd9f31a..db3ad074d2f 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -134,22 +134,13 @@ export default {
interopRightAttributes(props) {
return getInteropNewSideAttributes(props.line.right);
},
- conflictText: memoize(
- (line) => {
+ lineContent: (line) => {
+ if (line.isConflictMarker) {
return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes';
- },
- (line) => line.type,
- ),
- lineContent: memoize(
- (line) => {
- if (line.isConflictMarker) {
- return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes';
- }
+ }
- return line.rich_text;
- },
- (line) => line.line_code,
- ),
+ return line.rich_text;
+ },
CONFLICT_MARKER,
CONFLICT_MARKER_THEIR,
CONFLICT_OUR,
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 05d4fbe7c20..e8b4ff16aec 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -62,8 +62,8 @@ export default {
</div>
<div v-else class="diff-stats-contents">
<div v-if="hasDiffFiles" class="diff-stats-group">
- <gl-icon name="doc-code" class="diff-stats-icon text-secondary" />
- <span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
+ <gl-icon name="doc-code" class="diff-stats-icon gl-text-gray-500" />
+ <span class="gl-text-gray-500 bold">{{ diffFilesCountText }} {{ filesText }}</span>
</div>
<div
class="diff-stats-group gl-text-green-600 gl-display-flex gl-align-items-center"
diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue
index 178f93b651e..2d9ac76b3e4 100644
--- a/app/assets/javascripts/diffs/components/settings_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue
@@ -60,14 +60,14 @@ export default {
<gl-button
:class="{ selected: !renderTreeList }"
class="gl-w-half js-list-view"
- @click="setRenderTreeList(false)"
+ @click="setRenderTreeList({ renderTreeList: false })"
>
{{ __('List view') }}
</gl-button>
<gl-button
:class="{ selected: renderTreeList }"
class="gl-w-half js-tree-view"
- @click="setRenderTreeList(true)"
+ @click="setRenderTreeList({ renderTreeList: true })"
>
{{ __('Tree view') }}
</gl-button>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index f1cf556fde0..8dda5eadb16 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -111,6 +111,8 @@ export const CONFLICT_MARKER_OUR = 'conflict_marker_our';
export const CONFLICT_MARKER_THEIR = 'conflict_marker_their';
// Tracking events
+export const DEFER_DURATION = 750;
+
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';
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index a45fd92d0a9..e617890af2e 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -25,3 +25,25 @@ export const SETTINGS_DROPDOWN = {
fileByFile: __('Show one file at a time'),
preferences: __('Preferences'),
};
+
+export const CONFLICT_TEXT = {
+ both_modified: __('Conflict: This file was modified in both the source and target branches.'),
+ modified_source_removed_target: __(
+ 'Conflict: This file was modified in the source branch, but removed in the target branch.',
+ ),
+ modified_target_removed_source: __(
+ 'Conflict: This file was removed in the source branch, but modified in the target branch.',
+ ),
+ renamed_same_file: __(
+ 'Conflict: This file was renamed differently in the source and target branches.',
+ ),
+ removed_source_renamed_target: __(
+ 'Conflict: This file was removed in the source branch, but renamed in the target branch.',
+ ),
+ removed_target_renamed_source: __(
+ 'Conflict: This file was renamed in the source branch, but removed in the target branch.',
+ ),
+ both_added: __(
+ 'Conflict: This file was added both in the source and target branches, but with different contents.',
+ ),
+};
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index ea83523008c..bddc28c4758 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -2,6 +2,7 @@ import Cookies from 'js-cookie';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
@@ -82,6 +83,9 @@ export default function initDiffsApp(store) {
showWhitespaceDefault: parseBoolean(dataset.showWhitespaceDefault),
viewDiffsFileByFile: parseBoolean(dataset.fileByFileDefault),
defaultSuggestionCommitMessage: dataset.defaultSuggestionCommitMessage,
+ sourceProjectDefaultUrl: dataset.sourceProjectDefaultUrl,
+ sourceProjectFullPath: dataset.sourceProjectFullPath,
+ isForked: parseBoolean(dataset.isForked),
};
},
computed: {
@@ -93,7 +97,7 @@ export default function initDiffsApp(store) {
const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY);
const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true;
- this.setRenderTreeList(renderTreeList);
+ this.setRenderTreeList({ renderTreeList, trackClick: false });
// NOTE: A "true" or "checked" value for `showWhitespace` is '0' not '1'.
// Check for cookie and save that setting for future use.
@@ -104,6 +108,7 @@ export default function initDiffsApp(store) {
this.setShowWhitespace({
url: this.endpointUpdateUser,
showWhitespace: hideWhitespace !== '1',
+ trackClick: false,
});
Cookies.remove(DIFF_WHITESPACE_COOKIE_NAME);
} else {
@@ -111,8 +116,14 @@ export default function initDiffsApp(store) {
this.setShowWhitespace({
showWhitespace: this.showWhitespaceDefault,
updateDatabase: false,
+ trackClick: false,
});
}
+
+ const vScrollingParam = getParameterValues('virtual_scrolling')[0];
+ if (vScrollingParam === 'false' || vScrollingParam === 'true') {
+ Cookies.set('diffs_virtual_scrolling', vScrollingParam);
+ }
},
methods: {
...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']),
@@ -139,6 +150,9 @@ export default function initDiffsApp(store) {
fileByFileUserPreference: this.viewDiffsFileByFile,
defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage,
rehydratedMrReviews: getReviewsForMergeRequest(mrPath),
+ sourceProjectDefaultUrl: this.sourceProjectDefaultUrl,
+ sourceProjectFullPath: this.sourceProjectFullPath,
+ isForked: this.isForked,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 66510edf3db..f7bdbe94bac 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -1,6 +1,5 @@
import Cookies from 'js-cookie';
import Vue from 'vue';
-import api from '~/api';
import createFlash from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
@@ -50,6 +49,7 @@ import eventHub from '../event_hub';
import { isCollapsed } from '../utils/diff_file';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
import { getDerivedMergeRequestInformation } from '../utils/merge_request';
+import { queueRedisHllEvents } from '../utils/queue_events';
import TreeWorker from '../workers/tree_worker';
import * as types from './mutation_types';
import {
@@ -368,8 +368,7 @@ export const setInlineDiffViewType = ({ commit }) => {
historyPushState(url);
if (window.gon?.features?.diffSettingsUsageData) {
- api.trackRedisHllUserEvent(TRACKING_CLICK_DIFF_VIEW_SETTING);
- api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_INLINE);
+ queueRedisHllEvents([TRACKING_CLICK_DIFF_VIEW_SETTING, TRACKING_DIFF_VIEW_INLINE]);
}
};
@@ -381,8 +380,7 @@ export const setParallelDiffViewType = ({ commit }) => {
historyPushState(url);
if (window.gon?.features?.diffSettingsUsageData) {
- api.trackRedisHllUserEvent(TRACKING_CLICK_DIFF_VIEW_SETTING);
- api.trackRedisHllUserEvent(TRACKING_DIFF_VIEW_PARALLEL);
+ queueRedisHllEvents([TRACKING_CLICK_DIFF_VIEW_SETTING, TRACKING_DIFF_VIEW_PARALLEL]);
}
};
@@ -520,14 +518,14 @@ export const toggleActiveFileByHash = ({ commit }, hash) => {
commit(types.VIEW_DIFF_FILE, hash);
};
-export const scrollToFile = ({ state, commit }, path) => {
+export const scrollToFile = ({ state, commit, getters }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
commit(types.VIEW_DIFF_FILE, fileHash);
- if (window.gon?.features?.diffsVirtualScrolling) {
+ if (getters.isVirtualScrollingEnabled) {
eventHub.$emit('scrollToFileHash', fileHash);
setTimeout(() => {
@@ -535,6 +533,10 @@ export const scrollToFile = ({ state, commit }, path) => {
});
} else {
document.location.hash = fileHash;
+
+ setTimeout(() => {
+ handleLocationHash();
+ });
}
};
@@ -560,25 +562,27 @@ export const closeDiffFileCommentForm = ({ commit }, fileHash) => {
commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash);
};
-export const setRenderTreeList = ({ commit }, renderTreeList) => {
+export const setRenderTreeList = ({ commit }, { renderTreeList, trackClick = true }) => {
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 (window.gon?.features?.diffSettingsUsageData && trackClick) {
+ const events = [TRACKING_CLICK_FILE_BROWSER_SETTING];
if (renderTreeList) {
- api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
+ events.push(TRACKING_FILE_BROWSER_TREE);
} else {
- api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_LIST);
+ events.push(TRACKING_FILE_BROWSER_LIST);
}
+
+ queueRedisHllEvents(events);
}
};
export const setShowWhitespace = async (
{ state, commit },
- { url, showWhitespace, updateDatabase = true },
+ { url, showWhitespace, updateDatabase = true, trackClick = true },
) => {
if (updateDatabase && Boolean(window.gon?.current_user_id)) {
await axios.put(url || state.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace });
@@ -587,14 +591,16 @@ export const setShowWhitespace = async (
commit(types.SET_SHOW_WHITESPACE, showWhitespace);
notesEventHub.$emit('refetchDiffData');
- if (window.gon?.features?.diffSettingsUsageData) {
- api.trackRedisHllUserEvent(TRACKING_CLICK_WHITESPACE_SETTING);
+ if (window.gon?.features?.diffSettingsUsageData && trackClick) {
+ const events = [TRACKING_CLICK_WHITESPACE_SETTING];
if (showWhitespace) {
- api.trackRedisHllUserEvent(TRACKING_WHITESPACE_SHOW);
+ events.push(TRACKING_WHITESPACE_SHOW);
} else {
- api.trackRedisHllUserEvent(TRACKING_WHITESPACE_HIDE);
+ events.push(TRACKING_WHITESPACE_HIDE);
}
+
+ queueRedisHllEvents(events);
}
};
@@ -815,13 +821,15 @@ export const setFileByFile = ({ state, commit }, { fileByFile }) => {
Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
if (window.gon?.features?.diffSettingsUsageData) {
- api.trackRedisHllUserEvent(TRACKING_CLICK_SINGLE_FILE_SETTING);
+ const events = [TRACKING_CLICK_SINGLE_FILE_SETTING];
if (fileByFile) {
- api.trackRedisHllUserEvent(TRACKING_SINGLE_FILE_MODE);
+ events.push(TRACKING_SINGLE_FILE_MODE);
} else {
- api.trackRedisHllUserEvent(TRACKING_MULTIPLE_FILES_MODE);
+ events.push(TRACKING_MULTIPLE_FILES_MODE);
}
+
+ queueRedisHllEvents(events);
}
return axios
@@ -844,3 +852,5 @@ export function reviewFile({ commit, state }, { file, reviewed = true }) {
setReviewsForMergeRequest(mrPath, reviews);
commit(types.SET_MR_FILE_REVIEWS, reviews);
}
+
+export const disableVirtualScroller = ({ commit }) => commit(types.DISABLE_VIRTUAL_SCROLLING);
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 1b6a673925f..18bd8e5f1d8 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,3 +1,4 @@
+import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import {
@@ -173,7 +174,20 @@ export function suggestionCommitMessage(state, _, rootState) {
});
}
-export const isVirtualScrollingEnabled = (state) =>
- !state.viewDiffsFileByFile &&
- (window.gon?.features?.diffsVirtualScrolling ||
- getParameterValues('virtual_scrolling')[0] === 'true');
+export const isVirtualScrollingEnabled = (state) => {
+ const vSrollerCookie = Cookies.get('diffs_virtual_scrolling');
+
+ if (state.disableVirtualScroller) {
+ return false;
+ }
+
+ if (vSrollerCookie) {
+ return vSrollerCookie === 'true';
+ }
+
+ return (
+ !state.viewDiffsFileByFile &&
+ (window.gon?.features?.diffsVirtualScrolling ||
+ getParameterValues('virtual_scrolling')[0] === 'true')
+ );
+};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 348dd452698..d76361513d4 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -43,4 +43,5 @@ export default () => ({
defaultSuggestionCommitMessage: '',
mrReviews: {},
latestDiff: true,
+ disableVirtualScroller: false,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 4641731c4b6..2c370221f40 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -47,3 +47,4 @@ export const SET_DIFF_FILE_VIEWER = 'SET_DIFF_FILE_VIEWER';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
+export const DISABLE_VIRTUAL_SCROLLING = 'DISABLE_VIRTUAL_SCROLLING';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 9ff9a02d444..1aa83453bf7 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -362,4 +362,7 @@ export default {
[types.SET_MR_FILE_REVIEWS](state, newReviews) {
state.mrReviews = newReviews;
},
+ [types.DISABLE_VIRTUAL_SCROLLING](state) {
+ state.disableVirtualScroller = true;
+ },
};
diff --git a/app/assets/javascripts/diffs/utils/queue_events.js b/app/assets/javascripts/diffs/utils/queue_events.js
new file mode 100644
index 00000000000..08fcc98d45f
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/queue_events.js
@@ -0,0 +1,13 @@
+import { delay } from 'lodash';
+import api from '~/api';
+import { DEFER_DURATION } from '../constants';
+
+function trackRedisHllUserEvent(event, deferDuration = 0) {
+ delay(() => api.trackRedisHllUserEvent(event), deferDuration);
+}
+
+export function queueRedisHllEvents(events) {
+ events.forEach((event, index) => {
+ trackRedisHllUserEvent(event, DEFER_DURATION * (index + 1));
+ });
+}
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 849ff91841a..d40d19000fb 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -28,3 +28,9 @@ export const EDITOR_DIFF_INSTANCE_FN = 'createDiffInstance';
// '*.gitlab-ci.yml' regardless of project configuration.
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
+
+export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
+export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
+export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
+export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width
+export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
index 997503a12f5..76e009164f7 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
@@ -1,6 +1,165 @@
+import { debounce } from 'lodash';
+import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
+import createFlash from '~/flash';
+import { sanitize } from '~/lib/dompurify';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import syntaxHighlight from '~/syntax_highlight';
+import {
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS,
+ EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
+} from '../constants';
import { SourceEditorExtension } from './source_editor_extension_base';
+const getPreview = (text, projectPath = '') => {
+ let url;
+
+ if (projectPath) {
+ url = `/${projectPath}/preview_markdown`;
+ } else {
+ const { group, project } = document.body.dataset;
+ url = `/${group}/${project}/preview_markdown`;
+ }
+ return axios
+ .post(url, {
+ text,
+ })
+ .then(({ data }) => {
+ return data.body;
+ });
+};
+
+const setupDomElement = ({ injectToEl = null } = {}) => {
+ const previewEl = document.createElement('div');
+ previewEl.classList.add(EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS);
+ previewEl.style.display = 'none';
+ if (injectToEl) {
+ injectToEl.appendChild(previewEl);
+ }
+ return previewEl;
+};
+
export class EditorMarkdownExtension extends SourceEditorExtension {
+ constructor({ instance, projectPath, ...args } = {}) {
+ super({ instance, ...args });
+ Object.assign(instance, {
+ projectPath,
+ preview: {
+ el: undefined,
+ action: undefined,
+ shown: false,
+ modelChangeListener: undefined,
+ },
+ });
+ this.setupPreviewAction.call(instance);
+
+ instance.getModel().onDidChangeLanguage(({ newLanguage, oldLanguage } = {}) => {
+ if (newLanguage === 'markdown' && oldLanguage !== newLanguage) {
+ instance.setupPreviewAction();
+ } else {
+ instance.cleanup();
+ }
+ });
+
+ instance.onDidChangeModel(() => {
+ const model = instance.getModel();
+ if (model) {
+ const { language } = model.getLanguageIdentifier();
+ instance.cleanup();
+ if (language === 'markdown') {
+ instance.setupPreviewAction();
+ }
+ }
+ });
+ }
+
+ static togglePreviewLayout() {
+ const { width, height } = this.getLayoutInfo();
+ const newWidth = this.preview.shown
+ ? width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH
+ : width * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH;
+ this.layout({ width: newWidth, height });
+ }
+
+ static togglePreviewPanel() {
+ const parentEl = this.getDomNode().parentElement;
+ const { el: previewEl } = this.preview;
+ parentEl.classList.toggle(EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS);
+
+ if (previewEl.style.display === 'none') {
+ // Show the preview panel
+ this.fetchPreview();
+ } else {
+ // Hide the preview panel
+ previewEl.style.display = 'none';
+ }
+ }
+
+ cleanup() {
+ if (this.preview.modelChangeListener) {
+ this.preview.modelChangeListener.dispose();
+ }
+ this.preview.action.dispose();
+ if (this.preview.shown) {
+ EditorMarkdownExtension.togglePreviewPanel.call(this);
+ EditorMarkdownExtension.togglePreviewLayout.call(this);
+ }
+ this.preview.shown = false;
+ }
+
+ fetchPreview() {
+ const { el: previewEl } = this.preview;
+ getPreview(this.getValue(), this.projectPath)
+ .then((data) => {
+ previewEl.innerHTML = sanitize(data);
+ syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
+ previewEl.style.display = 'block';
+ })
+ .catch(() => createFlash(BLOB_PREVIEW_ERROR));
+ }
+
+ setupPreviewAction() {
+ if (this.getAction(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID)) return;
+
+ this.preview.action = this.addAction({
+ id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
+ label: __('Preview Markdown'),
+ keybindings: [
+ // eslint-disable-next-line no-bitwise,no-undef
+ monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_P),
+ ],
+ contextMenuGroupId: 'navigation',
+ contextMenuOrder: 1.5,
+
+ // Method that will be executed when the action is triggered.
+ // @param ed The editor instance is passed in as a convenience
+ run(instance) {
+ instance.togglePreview();
+ },
+ });
+ }
+
+ togglePreview() {
+ if (!this.preview?.el) {
+ this.preview.el = setupDomElement({ injectToEl: this.getDomNode().parentElement });
+ }
+ EditorMarkdownExtension.togglePreviewLayout.call(this);
+ EditorMarkdownExtension.togglePreviewPanel.call(this);
+
+ if (!this.preview?.shown) {
+ this.preview.modelChangeListener = this.onDidChangeModelContent(
+ debounce(this.fetchPreview.bind(this), EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY),
+ );
+ } else {
+ this.preview.modelChangeListener.dispose();
+ }
+
+ this.preview.shown = !this.preview?.shown;
+ }
+
getSelectedText(selection = this.getSelection()) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
const valArray = this.getValue().split('\n');
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index ee97714824e..81ddf8d77fa 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -1,7 +1,6 @@
-import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import { editor as monacoEditor, Uri } from 'monaco-editor';
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';
@@ -11,7 +10,7 @@ import {
EDITOR_READY_EVENT,
EDITOR_TYPE_DIFF,
} from './constants';
-import { clearDomElement } from './utils';
+import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils';
export default class SourceEditor {
constructor(options = {}) {
@@ -22,26 +21,11 @@ export default class SourceEditor {
...options,
};
- SourceEditor.setupMonacoTheme();
+ setupEditorTheme();
registerLanguages(...languages);
}
- static setupMonacoTheme() {
- const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
- const theme = themes.find((t) => t.name === themeName);
- if (theme) monacoEditor.defineTheme(themeName, theme.data);
- monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
- }
-
- static getModelLanguage(path) {
- const ext = `.${path.split('.').pop()}`;
- const language = monacoLanguages
- .getLanguages()
- .find((lang) => lang.extensions.indexOf(ext) !== -1);
- return language ? language.id : 'plaintext';
- }
-
static pushToImportsArray(arr, toImport) {
arr.push(import(toImport));
}
@@ -124,10 +108,7 @@ export default class SourceEditor {
return model;
}
const diffModel = {
- original: monacoEditor.createModel(
- blobOriginalContent,
- SourceEditor.getModelLanguage(model.uri.path),
- ),
+ original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)),
modified: model,
};
instance.setModel(diffModel);
@@ -155,7 +136,7 @@ export default class SourceEditor {
};
static instanceUpdateLanguage(inst, path) {
- const lang = SourceEditor.getModelLanguage(path);
+ const lang = getBlobLanguage(path);
const model = inst.getModel();
return monacoEditor.setModelLanguage(model, lang);
}
diff --git a/app/assets/javascripts/editor/utils.js b/app/assets/javascripts/editor/utils.js
index af4473413f4..df9d3f2b9fb 100644
--- a/app/assets/javascripts/editor/utils.js
+++ b/app/assets/javascripts/editor/utils.js
@@ -1,3 +1,6 @@
+import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
+import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
+
export const clearDomElement = (el) => {
if (!el || !el.firstChild) return;
@@ -6,6 +9,28 @@ export const clearDomElement = (el) => {
}
};
-export default () => ({
- clearDomElement,
-});
+export const setupEditorTheme = () => {
+ const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
+ const theme = themes.find((t) => t.name === themeName);
+ if (theme) monacoEditor.defineTheme(themeName, theme.data);
+ monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
+};
+
+export const getBlobLanguage = (blobPath) => {
+ const defaultLanguage = 'plaintext';
+
+ if (!blobPath) {
+ return defaultLanguage;
+ }
+
+ const ext = `.${blobPath.split('.').pop()}`;
+ const language = monacoLanguages
+ .getLanguages()
+ .find((lang) => lang.extensions.indexOf(ext) !== -1);
+ return language ? language.id : defaultLanguage;
+};
+
+export const setupCodeSnippet = (el) => {
+ monacoEditor.colorizeElement(el);
+ setupEditorTheme();
+};
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 76ad74e04d0..4783b92942c 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -1,29 +1,46 @@
<script>
-/* eslint-disable vue/no-v-html */
/**
* Render modal to confirm rollback/redeploy.
*/
-
-import { GlModal } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
import { escape } from 'lodash';
-import { s__, sprintf } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
name: 'ConfirmRollbackModal',
-
components: {
GlModal,
+ GlSprintf,
+ GlLink,
+ },
+ model: {
+ prop: 'visible',
+ event: 'change',
},
-
props: {
environment: {
type: Object,
required: true,
},
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ hasMultipleCommits: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ retryUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
-
computed: {
modalTitle() {
const title = this.environment.isLastDeployment
@@ -34,58 +51,47 @@ export default {
name: escape(this.environment.name),
});
},
-
commitShortSha() {
- const { last_deployment } = this.environment;
- return this.commitData(last_deployment, 'short_id');
- },
-
- commitUrl() {
- const { last_deployment } = this.environment;
- return this.commitData(last_deployment, 'commit_path');
- },
+ if (this.hasMultipleCommits) {
+ const { last_deployment } = this.environment;
+ return this.commitData(last_deployment, 'short_id');
+ }
- commitTitle() {
- const { last_deployment } = this.environment;
- return this.commitData(last_deployment, 'title');
+ return this.environment.commitShortSha;
},
+ commitUrl() {
+ if (this.hasMultipleCommits) {
+ const { last_deployment } = this.environment;
+ return this.commitData(last_deployment, 'commit_path');
+ }
- modalText() {
- const linkStart = `<a class="commit-sha mr-0" href="${escape(this.commitUrl)}">`;
- const commitId = escape(this.commitShortSha);
- const linkEnd = '</a>';
- const name = escape(this.name);
- const body = this.environment.isLastDeployment
- ? s__(
- 'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
- )
- : s__(
- 'Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?',
- );
- return sprintf(
- body,
- {
- commitId,
- linkStart,
- linkEnd,
- name,
- },
- false,
- );
+ return this.environment.commitUrl;
},
-
modalActionText() {
return this.environment.isLastDeployment
? s__('Environments|Re-deploy')
: s__('Environments|Rollback');
},
- },
+ primaryProps() {
+ let attributes = [{ variant: 'danger' }];
+
+ if (this.retryUrl) {
+ attributes = [...attributes, { 'data-method': 'post' }, { href: this.retryUrl }];
+ }
+ return {
+ text: this.modalActionText,
+ attributes,
+ };
+ },
+ },
methods: {
+ handleChange(event) {
+ this.$emit('change', event);
+ },
onOk() {
eventHub.$emit('rollbackEnvironment', this.environment);
},
-
commitData(lastDeployment, key) {
if (lastDeployment && lastDeployment.commit) {
return lastDeployment.commit[key];
@@ -94,16 +100,51 @@ export default {
return '';
},
},
+ csrf,
+ cancelProps: {
+ text: __('Cancel'),
+ attributes: [{ variant: 'danger' }],
+ },
};
</script>
<template>
<gl-modal
:title="modalTitle"
+ :visible="visible"
+ :action-cancel="$options.cancelProps"
+ :action-primary="primaryProps"
modal-id="confirm-rollback-modal"
- :ok-title="modalActionText"
- ok-variant="danger"
@ok="onOk"
+ @change="handleChange"
>
- <p v-html="modalText"></p>
+ <gl-sprintf
+ v-if="environment.isLastDeployment"
+ :message="
+ s__(
+ 'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
+ )
+ "
+ >
+ <template #link>
+ <gl-link :href="commitUrl" target="_blank" class="commit-sha mr-0">{{
+ commitShortSha
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <gl-sprintf
+ v-else
+ :message="
+ s__(
+ 'Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?',
+ )
+ "
+ >
+ <template #name>{{ environment.name }}</template>
+ <template #link>
+ <gl-link :href="commitUrl" target="_blank" class="commit-sha mr-0">{{
+ commitShortSha
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
new file mode 100644
index 00000000000..1cd960d7cd6
--- /dev/null
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -0,0 +1,58 @@
+<script>
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import EnvironmentForm from './environment_form.vue';
+
+export default {
+ components: {
+ EnvironmentForm,
+ },
+ inject: ['projectEnvironmentsPath', 'updateEnvironmentPath'],
+ props: {
+ environment: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ formEnvironment: {
+ name: this.environment.name,
+ externalUrl: this.environment.external_url,
+ },
+ loading: false,
+ };
+ },
+ methods: {
+ onChange(environment) {
+ this.formEnvironment = environment;
+ },
+ onSubmit() {
+ this.loading = true;
+ axios
+ .put(this.updateEnvironmentPath, {
+ id: this.environment.id,
+ name: this.formEnvironment.name,
+ external_url: this.formEnvironment.externalUrl,
+ })
+ .then(({ data: { path } }) => visitUrl(path))
+ .catch((error) => {
+ const message = error.response.data.message[0];
+ createFlash({ message });
+ this.loading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <environment-form
+ :cancel-path="projectEnvironmentsPath"
+ :environment="formEnvironment"
+ :title="__('Edit environment')"
+ :loading="loading"
+ @change="onChange"
+ @submit="onSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index b0c0f83b88a..d770a2302e8 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -1,5 +1,6 @@
<script>
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -25,6 +26,9 @@ export default {
step3: s__(
`EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
),
+ step4: s__(
+ `EnableReviewApp|%{stepStart}Step 4 (optional)%{stepEnd}. Enable Visual Reviews by following the %{linkStart}setup instructions%{linkEnd}.`,
+ ),
},
modalInfo: {
closeText: s__('EnableReviewApp|Close'),
@@ -45,6 +49,9 @@ export default {
except:
- ${this.defaultBranchName}`;
},
+ visualReviewsDocs() {
+ return helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' });
+ },
},
};
</script>
@@ -103,5 +110,15 @@ export default {
</template>
</gl-sprintf>
</p>
+ <p>
+ <gl-sprintf :message="$options.instructionText.step4">
+ <template #step="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #link="{ content }">
+ <gl-link :href="visualReviewsDocs" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
new file mode 100644
index 00000000000..6db8fe24e72
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -0,0 +1,146 @@
+<script>
+import { GlButton, GlForm, GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { isAbsolute } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ environment: {
+ required: true,
+ type: Object,
+ },
+ title: {
+ required: true,
+ type: String,
+ },
+ cancelPath: {
+ required: true,
+ type: String,
+ },
+ loading: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ },
+ i18n: {
+ header: __('Environments'),
+ helpMessage: __(
+ 'Environments allow you to track deployments of your application. %{linkStart}More information%{linkEnd}.',
+ ),
+ nameLabel: __('Name'),
+ nameFeedback: __('This field is required'),
+ urlLabel: __('External URL'),
+ urlFeedback: __('The URL should start with http:// or https://'),
+ save: __('Save'),
+ cancel: __('Cancel'),
+ },
+ helpPagePath: helpPagePath('ci/environments/index.md'),
+ data() {
+ return {
+ visited: {
+ name: null,
+ url: null,
+ },
+ };
+ },
+ computed: {
+ valid() {
+ return {
+ name: this.visited.name && this.environment.name !== '',
+ url: this.visited.url && isAbsolute(this.environment.externalUrl),
+ };
+ },
+ },
+ methods: {
+ onChange(env) {
+ this.$emit('change', env);
+ },
+ visit(field) {
+ this.visited[field] = true;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h3 class="page-title">
+ {{ title }}
+ </h3>
+ <hr />
+ <div class="row gl-mt-3 gl-mb-3">
+ <div class="col-lg-3">
+ <h4 class="gl-mt-0">
+ {{ $options.i18n.header }}
+ </h4>
+ <p>
+ <gl-sprintf :message="$options.i18n.helpMessage">
+ <template #link="{ content }">
+ <gl-link :href="$options.helpPagePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ <gl-form
+ id="new_environment"
+ :aria-label="title"
+ class="col-lg-9"
+ @submit.prevent="$emit('submit')"
+ >
+ <gl-form-group
+ :label="$options.i18n.nameLabel"
+ label-for="environment_name"
+ :state="valid.name"
+ :invalid-feedback="$options.i18n.nameFeedback"
+ >
+ <gl-form-input
+ id="environment_name"
+ :value="environment.name"
+ :state="valid.name"
+ name="environment[name]"
+ required
+ @input="onChange({ ...environment, name: $event })"
+ @blur="visit('name')"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.urlLabel"
+ :state="valid.url"
+ :invalid-feedback="$options.i18n.urlFeedback"
+ label-for="environment_external_url"
+ >
+ <gl-form-input
+ id="environment_external_url"
+ :value="environment.externalUrl"
+ :state="valid.url"
+ name="environment[external_url]"
+ type="url"
+ @input="onChange({ ...environment, externalUrl: $event })"
+ @blur="visit('url')"
+ />
+ </gl-form-group>
+
+ <div class="form-actions">
+ <gl-button
+ :loading="loading"
+ type="submit"
+ variant="confirm"
+ name="commit"
+ class="js-no-auto-disable"
+ >{{ $options.i18n.save }}</gl-button
+ >
+ <gl-button :href="cancelPath">{{ $options.i18n.cancel }}</gl-button>
+ </div>
+ </gl-form>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 5ae8b000fc0..897f6ce393e 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -776,23 +776,39 @@ export default {
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
- <pin-component v-if="canShowAutoStopDate" :auto-stop-url="autoStopUrl" />
+ <pin-component
+ v-if="canShowAutoStopDate"
+ :auto-stop-url="autoStopUrl"
+ data-track-action="click_button"
+ data-track-label="environment_pin"
+ />
<external-url-component
v-if="externalURL && canReadEnvironment"
:external-url="externalURL"
+ data-track-action="click_button"
+ data-track-label="environment_url"
/>
<monitoring-button-component
v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"
+ data-track-action="click_button"
+ data-track-label="environment_monitoring"
/>
- <actions-component v-if="actions.length > 0" :actions="actions" />
+ <actions-component
+ v-if="actions.length > 0"
+ :actions="actions"
+ data-track-action="click_dropdown"
+ data-track-label="environment_actions"
+ />
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
+ data-track-action="click_button"
+ data-track-label="environment_terminal"
/>
<rollback-component
@@ -800,11 +816,23 @@ export default {
:environment="model"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
+ data-track-action="click_button"
+ data-track-label="environment_rollback"
/>
- <stop-component v-if="canStopEnvironment" :environment="model" />
+ <stop-component
+ v-if="canStopEnvironment"
+ :environment="model"
+ data-track-action="click_button"
+ data-track-label="environment_stop"
+ />
- <delete-component v-if="canDeleteEnvironment" :environment="model" />
+ <delete-component
+ v-if="canDeleteEnvironment"
+ :environment="model"
+ data-track-action="click_button"
+ data-track-label="environment_delete"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index e4cf5760987..105315dcf51 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,7 +1,9 @@
<script>
-import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
+import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs, GlAlert } from '@gitlab/ui';
import createFlash from '~/flash';
+import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
+import { ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME } from '../constants';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
@@ -15,6 +17,12 @@ export default {
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
+ surveyAlertTitle: s__('Environments|Help us improve environments'),
+ surveyAlertText: s__(
+ 'Environments|Your feedback helps GitLab make environments better for you and other users. Participate and enter a sweepstake to win a USD 30 gift card.',
+ ),
+ surveyAlertButtonLabel: s__('Environments|Take the survey'),
+ surveyDismissButtonLabel: s__('Environments|Dismiss'),
},
modal: {
id: 'enable-review-app-info',
@@ -25,6 +33,7 @@ export default {
EnableReviewAppModal,
GlBadge,
GlButton,
+ GlAlert,
GlTab,
GlTabs,
StopEnvironmentModal,
@@ -56,6 +65,13 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ environmentsSurveyAlertDismissed: parseBoolean(
+ getCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME),
+ ),
+ };
+ },
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
@@ -105,6 +121,11 @@ export default {
openFolders.forEach((folder) => this.fetchChildEnvironments(folder));
}
},
+
+ onSurveyAlertDismiss() {
+ setCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME, 'true');
+ this.environmentsSurveyAlertDismissed = true;
+ },
},
};
</script>
@@ -135,6 +156,19 @@ export default {
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>
+ <gl-alert
+ v-if="!environmentsSurveyAlertDismissed"
+ class="gl-my-4"
+ :title="$options.i18n.surveyAlertTitle"
+ :primary-button-text="$options.i18n.surveyAlertButtonLabel"
+ variant="info"
+ dismissible
+ :dismiss-label="$options.i18n.surveyDismissButtonLabel"
+ primary-button-link="https://gitlab.fra1.qualtrics.com/jfe/form/SV_a2xyFsAA4D0w0Jg"
+ @dismiss="onSurveyAlertDismiss"
+ >
+ {{ $options.i18n.surveyAlertText }}
+ </gl-alert>
<gl-tabs :value="activeTab" content-class="gl-display-none">
<gl-tab
v-for="(tab, idx) in tabs"
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
new file mode 100644
index 00000000000..467c89fd8b8
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -0,0 +1,174 @@
+<script>
+import { GlButton, GlModalDirective, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __, s__ } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
+import StopEnvironmentModal from './stop_environment_modal.vue';
+
+export default {
+ name: 'EnvironmentsDetailHeader',
+ csrf,
+ components: {
+ GlButton,
+ GlSprintf,
+ TimeAgo,
+ DeleteEnvironmentModal,
+ StopEnvironmentModal,
+ },
+ directives: {
+ GlModalDirective,
+ GlTooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ environment: {
+ type: Object,
+ required: true,
+ },
+ canReadEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canUpdateEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canDestroyEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ canStopEnvironment: {
+ type: Boolean,
+ required: true,
+ },
+ cancelAutoStopPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ metricsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ terminalPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ i18n: {
+ autoStopAtText: s__('Environments|Auto stops %{autoStopAt}'),
+ metricsButtonTitle: __('See metrics'),
+ metricsButtonText: __('Monitoring'),
+ editButtonText: __('Edit'),
+ stopButtonText: s__('Environments|Stop'),
+ deleteButtonText: s__('Environments|Delete'),
+ externalButtonTitle: s__('Environments|Open live environment'),
+ externalButtonText: __('View deployment'),
+ cancelAutoStopButtonTitle: __('Prevent environment from auto-stopping'),
+ },
+ computed: {
+ shouldShowCancelAutoStopButton() {
+ return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
+ },
+ shouldShowExternalUrlButton() {
+ return this.canReadEnvironment && Boolean(this.environment.externalUrl);
+ },
+ shouldShowStopButton() {
+ return this.canStopEnvironment && this.environment.isAvailable;
+ },
+ shouldShowTerminalButton() {
+ return this.canAdminEnvironment && this.environment.hasTerminals;
+ },
+ },
+};
+</script>
+<template>
+ <header class="top-area gl-justify-content-between">
+ <div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
+ <h3 class="page-title">
+ {{ environment.name }}
+ </h3>
+ <p v-if="shouldShowCancelAutoStopButton" class="gl-mb-0 gl-ml-3" data-testid="auto-stops-at">
+ <gl-sprintf :message="$options.i18n.autoStopAtText">
+ <template #autoStopAt>
+ <time-ago :time="environment.autoStopAt" />
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ <div class="nav-controls gl-my-1">
+ <form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-button
+ v-if="shouldShowCancelAutoStopButton"
+ v-gl-tooltip.hover
+ data-testid="cancel-auto-stop-button"
+ :title="$options.i18n.cancelAutoStopButtonTitle"
+ type="submit"
+ icon="thumbtack"
+ />
+ </form>
+ <gl-button
+ v-if="shouldShowTerminalButton"
+ data-testid="terminal-button"
+ :href="terminalPath"
+ icon="terminal"
+ />
+ <gl-button
+ v-if="shouldShowExternalUrlButton"
+ v-gl-tooltip.hover
+ data-testid="external-url-button"
+ :title="$options.i18n.externalButtonTitle"
+ :href="environment.externalUrl"
+ icon="external-link"
+ target="_blank"
+ >{{ $options.i18n.externalButtonText }}</gl-button
+ >
+ <gl-button
+ v-if="canReadEnvironment"
+ data-testid="metrics-button"
+ :href="metricsPath"
+ :title="$options.i18n.metricsButtonTitle"
+ icon="chart"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.metricsButtonText }}
+ </gl-button>
+ <gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
+ {{ $options.i18n.editButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="shouldShowStopButton"
+ v-gl-modal-directive="'stop-environment-modal'"
+ data-testid="stop-button"
+ icon="stop"
+ variant="danger"
+ >
+ {{ $options.i18n.stopButtonText }}
+ </gl-button>
+ <gl-button
+ v-if="canDestroyEnvironment"
+ v-gl-modal-directive="'delete-environment-modal'"
+ data-testid="destroy-button"
+ variant="danger"
+ >
+ {{ $options.i18n.deleteButtonText }}
+ </gl-button>
+ </div>
+ <delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
+ <stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
+ </header>
+</template>
diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue
new file mode 100644
index 00000000000..14da2668417
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environment.vue
@@ -0,0 +1,51 @@
+<script>
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import EnvironmentForm from './environment_form.vue';
+
+export default {
+ components: {
+ EnvironmentForm,
+ },
+ inject: ['projectEnvironmentsPath'],
+ data() {
+ return {
+ environment: {
+ name: '',
+ externalUrl: '',
+ },
+ loading: false,
+ };
+ },
+ methods: {
+ onChange(env) {
+ this.environment = env;
+ },
+ onSubmit() {
+ this.loading = true;
+ axios
+ .post(this.projectEnvironmentsPath, {
+ name: this.environment.name,
+ external_url: this.environment.externalUrl,
+ })
+ .then(({ data: { path } }) => visitUrl(path))
+ .catch((error) => {
+ const message = error.response.data.message[0];
+ createFlash({ message });
+ this.loading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <environment-form
+ :cancel-path="projectEnvironmentsPath"
+ :environment="environment"
+ :title="__('New environment')"
+ :loading="loading"
+ @change="onChange($event)"
+ @submit="onSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/environments/components/rollback_modal_manager.vue b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
new file mode 100644
index 00000000000..6aa7d96fdfd
--- /dev/null
+++ b/app/assets/javascripts/environments/components/rollback_modal_manager.vue
@@ -0,0 +1,57 @@
+<script>
+import { parseBoolean } from '~/lib/utils/common_utils';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
+
+export default {
+ components: {
+ ConfirmRollbackModal,
+ },
+ props: {
+ selector: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ environment: null,
+ retryPath: '',
+ visible: false,
+ };
+ },
+ mounted() {
+ document.querySelectorAll(this.selector).forEach((button) => {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ const {
+ environmentName,
+ commitShortSha,
+ commitUrl,
+ isLastDeployment,
+ retryPath,
+ } = button.dataset;
+
+ this.environment = {
+ name: environmentName,
+ commitShortSha,
+ commitUrl,
+ isLastDeployment: parseBoolean(isLastDeployment),
+ };
+ this.retryPath = retryPath;
+ this.visible = true;
+ });
+ });
+ },
+};
+</script>
+
+<template>
+ <confirm-rollback-modal
+ v-if="environment"
+ v-model="visible"
+ :environment="environment"
+ :has-multiple-commits="false"
+ :retry-url="retryPath"
+ />
+ <div v-else></div>
+</template>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 6d427bef4e6..a02e72dfa72 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -38,3 +38,5 @@ export const CANARY_STATUS = {
};
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
+
+export const ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME = 'environments_survey_alert_dismissed';
diff --git a/app/assets/javascripts/environments/edit.js b/app/assets/javascripts/environments/edit.js
new file mode 100644
index 00000000000..dd6680f64bd
--- /dev/null
+++ b/app/assets/javascripts/environments/edit.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import EditEnvironment from './components/edit_environment.vue';
+
+export default (el) =>
+ new Vue({
+ el,
+ provide: {
+ projectEnvironmentsPath: el.dataset.projectEnvironmentsPath,
+ updateEnvironmentPath: el.dataset.updateEnvironmentPath,
+ },
+ render(h) {
+ return h(EditEnvironment, {
+ props: {
+ environment: JSON.parse(el.dataset.environment),
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/environments/init_confirm_rollback_modal.js b/app/assets/javascripts/environments/init_confirm_rollback_modal.js
new file mode 100644
index 00000000000..0161bb6078f
--- /dev/null
+++ b/app/assets/javascripts/environments/init_confirm_rollback_modal.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import RollbackModalManager from './components/rollback_modal_manager.vue';
+
+const mountConfirmRollbackModal = (optionalProps) =>
+ new Vue({
+ render(h) {
+ return h(RollbackModalManager, {
+ props: {
+ selector: '.js-confirm-rollback-modal-button',
+ ...optionalProps,
+ },
+ });
+ },
+ }).$mount();
+
+export default (optionalProps = {}) => mountConfirmRollbackModal(optionalProps);
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 6f701f87261..85cff73cc3e 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -108,7 +108,19 @@ export default {
this.service
.postAction(endpoint)
- .then(() => this.fetchEnvironments())
+ .then(() => {
+ // Originally, the detail page buttons were implemented as <form>s that POSTed
+ // to the server, which would naturally result in a page refresh.
+ // When environment details page was converted to Vue, the buttons were updated to trigger
+ // HTTP requests using `axios`, which did not cause a refresh on completion.
+ // To preserve the original behavior, we manually reload the page when
+ // network requests complete successfully.
+ if (!this.isDetailView) {
+ this.fetchEnvironments();
+ } else {
+ window.location.reload();
+ }
+ })
.catch((err) => {
this.isLoading = false;
createFlash({
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index d0b68b0c14f..f1c2dfec94b 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,30 +1,48 @@
import Vue from 'vue';
-import DeleteEnvironmentModal from './components/delete_environment_modal.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
import environmentsMixin from './mixins/environments_mixin';
-export default () => {
- const el = document.getElementById('delete-environment-modal');
+export const initHeader = () => {
+ const el = document.getElementById('environments-detail-view-header');
const container = document.getElementById('environments-detail-view');
+ const dataset = convertObjectPropsToCamelCase(JSON.parse(container.dataset.details));
return new Vue({
el,
- components: {
- DeleteEnvironmentModal,
- },
mixins: [environmentsMixin],
data() {
- const environment = JSON.parse(JSON.stringify(container.dataset));
- environment.delete_path = environment.deletePath;
- environment.onSingleEnvironmentPage = true;
+ const environment = {
+ name: dataset.name,
+ id: Number(dataset.id),
+ externalUrl: dataset.externalUrl,
+ isAvailable: dataset.isEnvironmentAvailable,
+ hasTerminals: dataset.hasTerminals,
+ autoStopAt: dataset.autoStopAt,
+ onSingleEnvironmentPage: true,
+ // TODO: These two props are snake_case because the environments_mixin file uses
+ // them and the mixin is imported in several files. It would be nice to conver them to camelCase.
+ stop_path: dataset.environmentStopPath,
+ delete_path: dataset.environmentDeletePath,
+ };
return {
environment,
};
},
render(createElement) {
- return createElement('delete-environment-modal', {
+ return createElement(EnvironmentsDetailHeader, {
props: {
environment: this.environment,
+ canDestroyEnvironment: dataset.canDestroyEnvironment,
+ canUpdateEnvironment: dataset.canUpdateEnvironment,
+ canReadEnvironment: dataset.canReadEnvironment,
+ canStopEnvironment: dataset.canStopEnvironment,
+ canAdminEnvironment: dataset.canAdminEnvironment,
+ cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
+ terminalPath: dataset.environmentTerminalPath,
+ metricsPath: dataset.environmentMetricsPath,
+ updatePath: dataset.environmentEditPath,
},
});
},
diff --git a/app/assets/javascripts/environments/new.js b/app/assets/javascripts/environments/new.js
new file mode 100644
index 00000000000..76aaf809d17
--- /dev/null
+++ b/app/assets/javascripts/environments/new.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import NewEnvironment from './components/new_environment.vue';
+
+export default (el) =>
+ new Vue({
+ el,
+ provide: { projectEnvironmentsPath: el.dataset.projectEnvironmentsPath },
+ render(h) {
+ return h(NewEnvironment);
+ },
+ });
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 4daf8b4e6bf..858c30649bb 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -25,19 +25,19 @@ export default {
},
stickinessOptions: [
{
- value: 'DEFAULT',
+ value: 'default',
text: __('Available ID'),
},
{
- value: 'USERID',
+ value: 'userId',
text: __('User ID'),
},
{
- value: 'SESSIONID',
+ value: 'sessionId',
text: __('Session ID'),
},
{
- value: 'RANDOM',
+ value: 'random',
text: __('Random'),
},
],
diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
deleted file mode 100644
index 42d0fbacca0..00000000000
--- a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { __ } from '~/locale';
-import FilteredSearchTokenKeys from './filtered_search_token_keys';
-
-const tokenKeys = [
- {
- formattedKey: __('Status'),
- key: 'status',
- type: 'string',
- param: 'status',
- symbol: '',
- icon: 'messages',
- tag: 'status',
- },
- {
- formattedKey: __('Type'),
- key: 'type',
- type: 'string',
- param: 'type',
- symbol: '',
- icon: 'cube',
- tag: 'type',
- },
- {
- formattedKey: __('Tag'),
- key: 'tag',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- icon: 'tag',
- tag: '~tag',
- },
-];
-
-const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
-
-export default AdminRunnersFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
deleted file mode 100644
index 9de18ba092f..00000000000
--- a/app/assets/javascripts/frequent_items/index.js
+++ /dev/null
@@ -1,77 +0,0 @@
-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);
-
-export default function initFrequentItemDropdowns() {
- 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`);
-
- // Don't do anything if element doesn't exist (No groups dropdown)
- // This is for when the user accesses GitLab without logging in
- if (!el || !navEl) {
- return;
- }
-
- import('./components/app.vue')
- .then(({ default: FrequentItems }) => {
- // eslint-disable-next-line no-new
- new Vue({
- el,
- store,
- data() {
- const { dataset } = this.$options.el;
- const item = {
- id: Number(dataset[`${key}Id`]),
- name: dataset[`${key}Name`],
- namespace: dataset[`${key}Namespace`],
- webUrl: dataset[`${key}WebUrl`],
- avatarUrl: dataset[`${key}AvatarUrl`] || null,
- lastAccessedOn: Date.now(),
- };
-
- return {
- currentUserName: dataset.userName,
- currentItem: item,
- };
- },
- render(createElement) {
- return createElement(
- VuexModuleProvider,
- {
- props: {
- vuexModule,
- },
- },
- [
- createElement(FrequentItems, {
- props: {
- namespace,
- currentUserName: this.currentUserName,
- currentItem: this.currentItem,
- searchClass: 'gl-display-none gl-sm-display-block',
- },
- }),
- ],
- );
- },
- });
- })
- .catch(() => {});
-
- $(navEl).on('shown.bs.dropdown', () => {
- eventHub.$emit(`${namespace}-dropdownOpen`);
- });
- });
-}
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index aad7712a9f0..312dd0c88dd 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -1,10 +1,12 @@
export const TYPE_CI_RUNNER = 'Ci::Runner';
+export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group';
export const TYPE_ISSUE = 'Issue';
export const TYPE_ITERATION = 'Iteration';
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
export const TYPE_MERGE_REQUEST = 'MergeRequest';
export const TYPE_MILESTONE = 'Milestone';
+export const TYPE_PROJECT = 'Project';
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
diff --git a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
index 6ed3be84cd8..3551394ff97 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issuable_timelogs.fragment.graphql
@@ -7,4 +7,5 @@ fragment TimelogFragment on Timelog {
note {
body
}
+ summary
}
diff --git a/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql
new file mode 100644
index 00000000000..79c56448b3f
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/mutations/create_merge_request.mutation.graphql
@@ -0,0 +1,8 @@
+mutation createMergeRequest($input: MergeRequestCreateInput!) {
+ mergeRequestCreate(input: $input) {
+ mergeRequest {
+ iid
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/get_users_projects.query.graphql b/app/assets/javascripts/graphql_shared/queries/get_users_projects.query.graphql
new file mode 100644
index 00000000000..58b7b4c898d
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/get_users_projects.query.graphql
@@ -0,0 +1,28 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getProjects(
+ $search: String!
+ $after: String = ""
+ $first: Int!
+ $searchNamespaces: Boolean = false
+ $sort: String
+ $membership: Boolean = true
+) {
+ projects(
+ search: $search
+ after: $after
+ first: $first
+ membership: $membership
+ searchNamespaces: $searchNamespaces
+ sort: $sort
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
new file mode 100644
index 00000000000..e345fe97281
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/group_users_search.query.graphql
@@ -0,0 +1,15 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query usersSearch($search: String!, $fullPath: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
+ nodes {
+ user {
+ ...User
+ ...UserAvailability
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 18f9a50bbce..828ddd95ffc 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -2,6 +2,21 @@ import { isArray } from 'lodash';
/**
* Ids generated by GraphQL endpoints are usually in the format
+ * gid://gitlab/Environments/123. This method checks if the passed id follows that format
+ *
+ * @param {String|Number} id The id value
+ * @returns {Boolean}
+ */
+export const isGid = (id) => {
+ if (typeof id === 'string' && id.startsWith('gid://gitlab/')) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Environments/123. This method extracts Id number
* from the Id path
*
@@ -35,6 +50,10 @@ export const convertToGraphQLId = (type, id) => {
throw new TypeError(`id must be a number or string; got ${typeof id}`);
}
+ if (isGid(id)) {
+ return id;
+ }
+
return `gid://gitlab/${type}/${id}`;
};
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index ad0b27c9693..10c45abbfa2 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -28,6 +28,10 @@ export default {
GlLoadingIcon,
GlIcon,
UserAccessRoleBadge,
+ ComplianceFrameworkLabel: () =>
+ import(
+ 'ee_component/vue_shared/components/compliance_framework_label/compliance_framework_label.vue'
+ ),
itemCaret,
itemTypeIcon,
itemStats,
@@ -67,6 +71,9 @@ export default {
hasAvatar() {
return this.group.avatarUrl !== null;
},
+ hasComplianceFramework() {
+ return Boolean(this.group.complianceFramework?.name);
+ },
isGroup() {
return this.group.type === 'group';
},
@@ -82,6 +89,9 @@ export default {
microdata() {
return this.group.microdata || {};
},
+ complianceFramework() {
+ return this.group.complianceFramework;
+ },
},
methods: {
onClickRowGroup(e) {
@@ -167,6 +177,13 @@ export default {
<user-access-role-badge v-if="group.permission" class="gl-mt-3">
{{ group.permission }}
</user-access-role-badge>
+ <compliance-framework-label
+ v-if="hasComplianceFramework"
+ class="gl-mt-3"
+ :name="complianceFramework.name"
+ :color="complianceFramework.color"
+ :description="complianceFramework.description"
+ />
</div>
<div v-if="group.description" class="description">
<span
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 6cf70f4052e..93fbd8be47d 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -1,4 +1,5 @@
-import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
+import { isEmpty } from 'lodash';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import { getGroupItemMicrodata } from './utils';
export default class GroupsStore {
@@ -70,7 +71,7 @@ export default class GroupsStore {
? rawGroupItem.subgroup_count
: rawGroupItem.children_count;
- return {
+ const groupItem = {
id: rawGroupItem.id,
name: rawGroupItem.name,
fullName: rawGroupItem.full_name,
@@ -98,6 +99,16 @@ export default class GroupsStore {
pendingRemoval: rawGroupItem.marked_for_deletion,
microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {},
};
+
+ if (!isEmpty(rawGroupItem.compliance_management_framework)) {
+ groupItem.complianceFramework = {
+ name: rawGroupItem.compliance_management_framework.name,
+ color: rawGroupItem.compliance_management_framework.color,
+ description: rawGroupItem.compliance_management_framework.description,
+ };
+ }
+
+ return groupItem;
}
removeGroup(group, parentGroup) {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 2897f4cbf77..9ec4a07a3d0 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -186,7 +186,7 @@ export default {
data-testid="commit-button"
class="qa-commit-button"
category="primary"
- variant="success"
+ variant="confirm"
@click="commit"
>
{{ __('Commit') }}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index fbe353fc4ba..829686ef051 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,12 +1,12 @@
<script>
-import { GlModal, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import ListItem from './list_item.vue';
export default {
components: {
- GlIcon,
+ GlButton,
ListItem,
GlModal,
},
@@ -70,7 +70,7 @@ export default {
<div class="d-flex align-items-center flex-fill">
<strong> {{ titleText }} </strong>
<div class="d-flex ml-auto">
- <button
+ <gl-button
v-if="!stagedList"
v-gl-tooltip
:title="__('Discard all changes')"
@@ -79,15 +79,14 @@ export default {
:class="{
'disabled-content': !filesLength,
}"
- type="button"
- class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
+ class="gl-shadow-none!"
+ category="tertiary"
+ icon="remove-all"
data-placement="bottom"
data-container="body"
data-boundary="viewport"
@click="openDiscardModal"
- >
- <gl-icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
- </button>
+ />
</div>
</div>
</header>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5c711313ff6..bf5ec849bc5 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -38,6 +38,8 @@ import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '..
import FileAlert from './file_alert.vue';
import FileTemplatesBar from './file_templates/bar.vue';
+const MARKDOWN_FILE_TYPE = 'markdown';
+
export default {
name: 'RepoEditor',
components: {
@@ -201,7 +203,7 @@ export default {
showContentViewer(val) {
if (!val) return;
- if (this.fileType === 'markdown') {
+ if (this.fileType === MARKDOWN_FILE_TYPE) {
const { content, images } = extractMarkdownImagesFromEntries(this.file, this.entries);
this.content = content;
this.images = images;
@@ -309,6 +311,23 @@ export default {
}),
);
+ if (this.fileType === MARKDOWN_FILE_TYPE) {
+ import('~/editor/extensions/source_editor_markdown_ext')
+ .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
+ this.editor.use(
+ new MarkdownExtension({
+ instance: this.editor,
+ projectPath: this.currentProjectId,
+ }),
+ );
+ })
+ .catch((e) =>
+ createFlash({
+ message: e,
+ }),
+ );
+ }
+
this.$nextTick(() => {
this.setupEditor();
});
@@ -406,7 +425,11 @@ export default {
const reImage = /^image\/(png|jpg|jpeg|gif)$/;
const file = event.clipboardData.files[0];
- if (editor.hasTextFocus() && this.fileType === 'markdown' && reImage.test(file?.type)) {
+ if (
+ editor.hasTextFocus() &&
+ this.fileType === MARKDOWN_FILE_TYPE &&
+ reImage.test(file?.type)
+ ) {
// don't let the event be passed on to Monaco.
event.preventDefault();
event.stopImmediatePropagation();
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 44d6d17232f..5ba910746ca 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -28,7 +28,7 @@ export default {
<template>
<gl-dropdown
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1"
+ class="gl-h-7 gl-flex-fill-1"
data-qa-selector="target_namespace_selector_dropdown"
v-bind="$attrs"
>
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 cb7e3ef9632..db44be2bcd7 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
@@ -10,20 +10,25 @@ import {
GlSearchBoxByClick,
GlSprintf,
GlSafeHtmlDirective as SafeHtml,
- GlTooltip,
+ GlTable,
+ GlFormCheckbox,
} from '@gitlab/ui';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import ImportStatus from '../../components/import_status.vue';
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 setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import ImportTableRow from './import_table_row.vue';
+import { isInvalid } from '../utils';
+import ImportTargetCell from './import_target_cell.vue';
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
+const DEFAULT_TH_CLASSES =
+ 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
+const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
@@ -35,9 +40,11 @@ export default {
GlLink,
GlLoadingIcon,
GlSearchBoxByClick,
+ GlFormCheckbox,
GlSprintf,
- GlTooltip,
- ImportTableRow,
+ GlTable,
+ ImportStatus,
+ ImportTargetCell,
PaginationLinks,
},
directives: {
@@ -53,6 +60,10 @@ export default {
type: RegExp,
required: true,
},
+ groupUrlErrorMessage: {
+ type: String,
+ required: true,
+ },
},
data() {
@@ -60,6 +71,7 @@ export default {
filter: '',
page: 1,
perPage: DEFAULT_PAGE_SIZE,
+ selectedGroups: [],
};
},
@@ -73,21 +85,58 @@ export default {
availableNamespaces: availableNamespacesQuery,
},
+ fields: [
+ {
+ key: 'selected',
+ label: '',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
+ },
+ {
+ key: 'web_url',
+ label: s__('BulkImport|From source group'),
+ thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
+ },
+ {
+ key: 'import_target',
+ label: s__('BulkImport|To new group'),
+ thClass: `${DEFAULT_TH_CLASSES} import-jobs-to-col`,
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'progress',
+ label: __('Status'),
+ thClass: `${DEFAULT_TH_CLASSES} import-jobs-status-col`,
+ tdClass: DEFAULT_TD_CLASSES,
+ tdAttr: { 'data-qa-selector': 'import_status_indicator' },
+ },
+ {
+ key: 'actions',
+ label: '',
+ thClass: `${DEFAULT_TH_CLASSES} import-jobs-cta-col`,
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ ],
+
computed: {
groups() {
return this.bulkImportSourceGroups?.nodes ?? [];
},
- hasGroupsWithValidationError() {
- return this.groups.some((g) => g.validation_errors.length);
+ hasSelectedGroups() {
+ return this.selectedGroups.length > 0;
},
- availableGroupsForImport() {
- return this.groups.filter((g) => g.progress.status === STATUSES.NONE);
+ hasAllAvailableGroupsSelected() {
+ return this.selectedGroups.length === this.availableGroupsForImport.length;
},
- isImportAllButtonDisabled() {
- return this.hasGroupsWithValidationError || this.availableGroupsForImport.length === 0;
+ availableGroupsForImport() {
+ return this.groups.filter((g) => g.progress.status === STATUSES.NONE && !this.isInvalid(g));
},
humanizedTotal() {
@@ -117,7 +166,7 @@ export default {
total: 0,
};
const start = (page - 1) * perPage + 1;
- const end = start + (this.bulkImportSourceGroups.nodes?.length ?? 0) - 1;
+ const end = start + this.groups.length - 1;
return { start, end, total };
},
@@ -127,9 +176,39 @@ export default {
filter() {
this.page = 1;
},
+ groups() {
+ const table = this.getTableRef();
+ this.groups.forEach((g, idx) => {
+ if (this.selectedGroups.includes(g)) {
+ this.$nextTick(() => {
+ table.selectRow(idx);
+ });
+ }
+ });
+ this.selectedGroups = [];
+ },
},
methods: {
+ qaRowAttributes(group, type) {
+ if (type === 'row') {
+ return {
+ 'data-qa-selector': 'import_item',
+ 'data-qa-source-group': group.full_path,
+ };
+ }
+
+ return {};
+ },
+
+ isAlreadyImported(group) {
+ return group.progress.status !== STATUSES.NONE;
+ },
+
+ isInvalid(group) {
+ return isInvalid(group, this.groupPathRegex);
+ },
+
groupsCount(count) {
return n__('%d group', '%d groups', count);
},
@@ -138,17 +217,10 @@ export default {
this.page = page;
},
- updateTargetNamespace(sourceGroupId, targetNamespace) {
+ updateImportTarget(sourceGroupId, targetNamespace, newName) {
this.$apollo.mutate({
- mutation: setTargetNamespaceMutation,
- variables: { sourceGroupId, targetNamespace },
- });
- },
-
- updateNewName(sourceGroupId, newName) {
- this.$apollo.mutate({
- mutation: setNewNameMutation,
- variables: { sourceGroupId, newName },
+ mutation: setImportTargetMutation,
+ variables: { sourceGroupId, targetNamespace, newName },
});
},
@@ -159,13 +231,33 @@ export default {
});
},
- importAllGroups() {
- this.importGroups(this.availableGroupsForImport.map((g) => g.id));
+ importSelectedGroups() {
+ this.importGroups(this.selectedGroups.map((g) => g.id));
},
setPageSize(size) {
this.perPage = size;
},
+
+ getTableRef() {
+ // Acquire reference to BTable to manipulate selection
+ // issue: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1531
+ // refs are not reactive, so do not use computed here
+ return this.$refs.table?.$children[0];
+ },
+
+ preventSelectingAlreadyImportedGroups(updatedSelection) {
+ if (updatedSelection) {
+ this.selectedGroups = updatedSelection;
+ }
+
+ const table = this.getTableRef();
+ this.groups.forEach((group, idx) => {
+ if (table.isRowSelected(idx) && (this.isAlreadyImported(group) || this.isInvalid(group))) {
+ table.unselectRow(idx);
+ }
+ });
+ },
},
gitlabLogo: window.gon.gitlab_logo,
@@ -180,28 +272,6 @@ export default {
>
<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"
@@ -247,27 +317,92 @@ export default {
:description="s__('Check your source instance permissions.')"
/>
<template v-else>
- <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>
- <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th>
- <th class="gl-py-4 import-jobs-cta-col"></th>
- </thead>
- <tbody class="gl-vertical-align-top">
- <template v-for="group in bulkImportSourceGroups.nodes">
- <import-table-row
- :key="group.id"
- :group="group"
- :available-namespaces="availableNamespaces"
- :group-path-regex="groupPathRegex"
- @update-target-namespace="updateTargetNamespace(group.id, $event)"
- @update-new-name="updateNewName(group.id, $event)"
- @import-group="importGroups([group.id])"
- />
+ <div
+ class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-p-4 gl-display-flex gl-align-items-center"
+ >
+ <gl-sprintf :message="__('%{count} selected')">
+ <template #count>
+ {{ selectedGroups.length }}
</template>
- </tbody>
- </table>
+ </gl-sprintf>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ class="gl-ml-4"
+ :disabled="!hasSelectedGroups"
+ @click="importSelectedGroups"
+ >{{ s__('BulkImport|Import selected') }}</gl-button
+ >
+ </div>
+ <gl-table
+ ref="table"
+ class="gl-w-full"
+ data-qa-selector="import_table"
+ tbody-tr-class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"
+ :tbody-tr-attr="qaRowAttributes"
+ :items="groups"
+ :fields="$options.fields"
+ selectable
+ select-mode="multi"
+ selected-variant="primary"
+ @row-selected="preventSelectingAlreadyImportedGroups"
+ >
+ <template #head(selected)="{ selectAllRows, clearSelected }">
+ <gl-form-checkbox
+ :key="`checkbox-${selectedGroups.length}`"
+ class="gl-h-7 gl-pt-3"
+ :checked="hasSelectedGroups"
+ :indeterminate="hasSelectedGroups && !hasAllAvailableGroupsSelected"
+ @change="hasAllAvailableGroupsSelected ? clearSelected() : selectAllRows()"
+ />
+ </template>
+ <template #cell(selected)="{ rowSelected, selectRow, unselectRow, item: group }">
+ <gl-form-checkbox
+ class="gl-h-7 gl-pt-3"
+ :checked="rowSelected"
+ :disabled="isAlreadyImported(group) || isInvalid(group)"
+ @change="rowSelected ? unselectRow() : selectRow()"
+ />
+ </template>
+ <template #cell(web_url)="{ value: web_url, item: { full_path } }">
+ <gl-link
+ :href="web_url"
+ target="_blank"
+ class="gl-display-inline-flex gl-align-items-center gl-h-7"
+ >
+ {{ full_path }} <gl-icon name="external-link" />
+ </gl-link>
+ </template>
+ <template #cell(import_target)="{ item: group }">
+ <import-target-cell
+ :group="group"
+ :available-namespaces="availableNamespaces"
+ :group-path-regex="groupPathRegex"
+ :group-url-error-message="groupUrlErrorMessage"
+ @update-target-namespace="
+ updateImportTarget(group.id, $event, group.import_target.new_name)
+ "
+ @update-new-name="
+ updateImportTarget(group.id, group.import_target.target_namespace, $event)
+ "
+ />
+ </template>
+ <template #cell(progress)="{ value: { status } }">
+ <import-status :status="status" class="gl-line-height-32" />
+ </template>
+ <template #cell(actions)="{ item: group }">
+ <gl-button
+ v-if="!isAlreadyImported(group)"
+ :disabled="isInvalid(group)"
+ variant="confirm"
+ category="secondary"
+ data-qa-selector="import_group_button"
+ @click="importGroups([group.id])"
+ >
+ {{ __('Import') }}
+ </gl-button>
+ </template>
+ </gl-table>
<div v-if="hasGroups" class="gl-display-flex gl-mt-3 gl-align-items-center">
<pagination-links
:change="setPage"
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
deleted file mode 100644
index 1c3ede769e0..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
+++ /dev/null
@@ -1,227 +0,0 @@
-<script>
-import {
- GlButton,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlIcon,
- GlLink,
- GlFormInput,
-} from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
-import ImportGroupDropdown from '../../components/group_dropdown.vue';
-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 groupAndProjectQuery from '../graphql/queries/groupAndProject.query.graphql';
-
-const DEBOUNCE_INTERVAL = 300;
-
-export default {
- components: {
- ImportStatus,
- ImportGroupDropdown,
- GlButton,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlLink,
- GlIcon,
- GlFormInput,
- },
- props: {
- group: {
- type: Object,
- required: true,
- },
- availableNamespaces: {
- type: Array,
- required: true,
- },
- groupPathRegex: {
- type: RegExp,
- required: true,
- },
- },
-
- apollo: {
- existingGroupAndProject: {
- query: groupAndProjectQuery,
- debounce: DEBOUNCE_INTERVAL,
- variables() {
- return {
- fullPath: this.fullPath,
- };
- },
- update({ existingGroup, existingProject }) {
- const variables = {
- field: 'new_name',
- sourceGroupId: this.group.id,
- };
-
- if (!existingGroup && !existingProject) {
- this.$apollo.mutate({
- mutation: removeValidationErrorMutation,
- variables,
- });
- } else {
- this.$apollo.mutate({
- mutation: addValidationErrorMutation,
- variables: {
- ...variables,
- message: this.$options.i18n.NAME_ALREADY_EXISTS,
- },
- });
- }
- },
- skip() {
- return !this.isNameValid || this.isAlreadyImported;
- },
- },
- },
-
- computed: {
- availableNamespaceNames() {
- return this.availableNamespaces.map((ns) => ns.full_path);
- },
-
- importTarget() {
- return this.group.import_target;
- },
-
- invalidNameValidationMessage() {
- return this.group.validation_errors.find(({ field }) => field === 'new_name')?.message;
- },
-
- isInvalid() {
- return Boolean(!this.isNameValid || this.invalidNameValidationMessage);
- },
-
- isNameValid() {
- return this.groupPathRegex.test(this.importTarget.new_name);
- },
-
- isAlreadyImported() {
- return this.group.progress.status !== STATUSES.NONE;
- },
-
- isFinished() {
- return this.group.progress.status === STATUSES.FINISHED;
- },
-
- fullPath() {
- return `${this.importTarget.target_namespace}/${this.importTarget.new_name}`;
- },
-
- absolutePath() {
- return joinPaths(gon.relative_url_root || '/', this.fullPath);
- },
- },
-
- i18n: {
- NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
- },
-};
-</script>
-
-<template>
- <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"
- target="_blank"
- class="gl-display-flex gl-align-items-center gl-h-7"
- >
- {{ group.full_path }} <gl-icon name="external-link" />
- </gl-link>
- </td>
- <td class="gl-p-4">
- <gl-link
- v-if="isFinished"
- class="gl-display-flex gl-align-items-center gl-h-7"
- :href="absolutePath"
- >
- {{ fullPath }}
- </gl-link>
-
- <div
- v-else
- class="import-entities-target-select gl-display-flex gl-align-items-stretch"
- :class="{
- disabled: isAlreadyImported,
- }"
- >
- <import-group-dropdown
- #default="{ namespaces }"
- :text="importTarget.target_namespace"
- :disabled="isAlreadyImported"
- :namespaces="availableNamespaceNames"
- toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
- class="import-entities-namespace-dropdown gl-h-7 gl-flex-grow-1"
- data-qa-selector="target_namespace_selector_dropdown"
- >
- <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
- s__('BulkImport|No parent')
- }}</gl-dropdown-item>
- <template v-if="namespaces.length">
- <gl-dropdown-divider />
- <gl-dropdown-section-header>
- {{ s__('BulkImport|Existing groups') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="ns in namespaces"
- :key="ns"
- data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns"
- @click="$emit('update-target-namespace', ns)"
- >
- {{ ns }}
- </gl-dropdown-item>
- </template>
- </import-group-dropdown>
- <div
- class="import-entities-target-select-separator gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
- >
- /
- </div>
- <div class="gl-flex-grow-1">
- <gl-form-input
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
- :class="{ 'is-invalid': isInvalid && !isAlreadyImported }"
- :disabled="isAlreadyImported"
- :value="importTarget.new_name"
- @input="$emit('update-new-name', $event)"
- />
- <p v-if="isInvalid" class="gl-text-red-500 gl-m-0 gl-mt-2">
- <template v-if="!isNameValid">
- {{ __('Please choose a group URL with no special characters.') }}
- </template>
- <template v-else-if="invalidNameValidationMessage">
- {{ invalidNameValidationMessage }}
- </template>
- </p>
- </div>
- </div>
- </td>
- <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="confirm"
- category="secondary"
- data-qa-selector="import_group_button"
- @click="$emit('import-group')"
- >{{ __('Import') }}</gl-button
- >
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
new file mode 100644
index 00000000000..7359d4f239e
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -0,0 +1,162 @@
+<script>
+import {
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLink,
+ GlFormInput,
+} from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import ImportGroupDropdown from '../../components/group_dropdown.vue';
+import { STATUSES } from '../../constants';
+import { isInvalid, getInvalidNameValidationMessage, isNameValid } from '../utils';
+
+export default {
+ components: {
+ ImportGroupDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLink,
+ GlFormInput,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ availableNamespaces: {
+ type: Array,
+ required: true,
+ },
+ groupPathRegex: {
+ type: RegExp,
+ required: true,
+ },
+ groupUrlErrorMessage: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ availableNamespaceNames() {
+ return this.availableNamespaces.map((ns) => ns.full_path);
+ },
+
+ importTarget() {
+ return this.group.import_target;
+ },
+
+ invalidNameValidationMessage() {
+ return getInvalidNameValidationMessage(this.group);
+ },
+
+ isInvalid() {
+ return isInvalid(this.group, this.groupPathRegex);
+ },
+
+ isNameValid() {
+ return isNameValid(this.group, this.groupPathRegex);
+ },
+
+ isAlreadyImported() {
+ return this.group.progress.status !== STATUSES.NONE;
+ },
+
+ isFinished() {
+ return this.group.progress.status === STATUSES.FINISHED;
+ },
+
+ fullPath() {
+ return `${this.importTarget.target_namespace}/${this.importTarget.new_name}`;
+ },
+
+ absolutePath() {
+ return joinPaths(gon.relative_url_root || '/', this.fullPath);
+ },
+ },
+
+ i18n: {
+ NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+ },
+};
+</script>
+
+<template>
+ <gl-link
+ v-if="isFinished"
+ class="gl-display-inline-flex gl-align-items-center gl-h-7"
+ :href="absolutePath"
+ >
+ {{ fullPath }}
+ </gl-link>
+
+ <div
+ v-else
+ class="gl-display-flex gl-align-items-stretch"
+ :class="{
+ disabled: isAlreadyImported,
+ }"
+ >
+ <import-group-dropdown
+ #default="{ namespaces }"
+ :text="importTarget.target_namespace"
+ :disabled="isAlreadyImported"
+ :namespaces="availableNamespaceNames"
+ toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ class="gl-h-7 gl-flex-grow-1"
+ data-qa-selector="target_namespace_selector_dropdown"
+ >
+ <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
+ s__('BulkImport|No parent')
+ }}</gl-dropdown-item>
+ <template v-if="namespaces.length">
+ <gl-dropdown-divider />
+ <gl-dropdown-section-header>
+ {{ s__('BulkImport|Existing groups') }}
+ </gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="ns in namespaces"
+ :key="ns"
+ data-qa-selector="target_group_dropdown_item"
+ :data-qa-group-name="ns"
+ @click="$emit('update-target-namespace', ns)"
+ >
+ {{ ns }}
+ </gl-dropdown-item>
+ </template>
+ </import-group-dropdown>
+ <div
+ class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
+ :class="{
+ 'gl-text-gray-400 gl-border-gray-100': isAlreadyImported,
+ 'gl-border-gray-200': !isAlreadyImported,
+ }"
+ >
+ /
+ </div>
+ <div class="gl-flex-grow-1">
+ <gl-form-input
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
+ :class="{
+ 'gl-inset-border-1-gray-200!': !isAlreadyImported,
+ 'gl-inset-border-1-gray-100!': isAlreadyImported,
+ 'is-invalid': isInvalid && !isAlreadyImported,
+ }"
+ :disabled="isAlreadyImported"
+ :value="importTarget.new_name"
+ @input="$emit('update-new-name', $event)"
+ />
+ <p v-if="isInvalid" class="gl-text-red-500 gl-m-0 gl-mt-2">
+ <template v-if="!isNameValid">
+ {{ groupUrlErrorMessage }}
+ </template>
+ <template v-else-if="invalidNameValidationMessage">
+ {{ invalidNameValidationMessage }}
+ </template>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js
new file mode 100644
index 00000000000..b2c3d85e280
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/constants.js
@@ -0,0 +1,7 @@
+import { s__ } from '~/locale';
+
+export const i18n = {
+ NAME_ALREADY_EXISTS: s__('BulkImport|Name already exists.'),
+};
+
+export const NEW_NAME_FIELD = 'new_name';
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 2cde3781a6a..57188441158 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,11 +4,15 @@ import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
+import { i18n, NEW_NAME_FIELD } from '../constants';
import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
+import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql';
+import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.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 groupAndProjectQuery from './queries/group_and_project.query.graphql';
import { SourceGroupsManager } from './services/source_groups_manager';
import { StatusPoller } from './services/status_poller';
import typeDefs from './typedefs.graphql';
@@ -46,6 +50,37 @@ function makeGroup(data) {
return result;
}
+async function checkImportTargetIsValid({ client, newName, targetNamespace, sourceGroupId }) {
+ const {
+ data: { existingGroup, existingProject },
+ } = await client.query({
+ query: groupAndProjectQuery,
+ variables: {
+ fullPath: `${targetNamespace}/${newName}`,
+ },
+ });
+
+ const variables = {
+ field: NEW_NAME_FIELD,
+ sourceGroupId,
+ };
+
+ if (!existingGroup && !existingProject) {
+ client.mutate({
+ mutation: removeValidationErrorMutation,
+ variables,
+ });
+ } else {
+ client.mutate({
+ mutation: addValidationErrorMutation,
+ variables: {
+ ...variables,
+ message: i18n.NAME_ALREADY_EXISTS,
+ },
+ });
+ }
+}
+
const localProgressId = (id) => `not-started-${id}`;
export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
@@ -99,7 +134,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
]) => {
const pagination = parseIntPagination(normalizeHeaders(headers));
- return {
+ const response = {
__typename: clientTypenames.BulkImportSourceGroupConnection,
nodes: data.importable_data.map((group) => {
const { jobId, importState: cachedImportState } =
@@ -123,6 +158,21 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
...pagination,
},
};
+
+ setTimeout(() => {
+ response.nodes.forEach((group) => {
+ if (group.progress.status === STATUSES.NONE) {
+ checkImportTargetIsValid({
+ client,
+ newName: group.import_target.new_name,
+ targetNamespace: group.import_target.target_namespace,
+ sourceGroupId: group.id,
+ });
+ }
+ });
+ });
+
+ return response;
},
);
},
@@ -136,6 +186,22 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
),
},
Mutation: {
+ setImportTarget(_, { targetNamespace, newName, sourceGroupId }, { client }) {
+ checkImportTargetIsValid({
+ client,
+ sourceGroupId,
+ targetNamespace,
+ newName,
+ });
+ return makeGroup({
+ id: sourceGroupId,
+ import_target: {
+ target_namespace: targetNamespace,
+ new_name: newName,
+ },
+ });
+ },
+
setTargetNamespace: (_, { targetNamespace, sourceGroupId }) =>
makeGroup({
id: sourceGroupId,
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
new file mode 100644
index 00000000000..793b60ee378
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_target.mutation.graphql
@@ -0,0 +1,13 @@
+mutation setImportTarget($newName: String!, $targetNamespace: String!, $sourceGroupId: String!) {
+ setImportTarget(
+ newName: $newName
+ targetNamespace: $targetNamespace
+ sourceGroupId: $sourceGroupId
+ ) @client {
+ id
+ import_target {
+ new_name
+ target_namespace
+ }
+ }
+}
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
deleted file mode 100644
index 354bf2a5815..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-mutation setNewName($newName: String!, $sourceGroupId: String!) {
- 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
deleted file mode 100644
index a0ef407f135..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) {
- setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client {
- id
- import_target {
- target_namespace
- }
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
index d6124f84025..d6124f84025 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/group_and_project.query.graphql
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index cc60c8cbdb0..07b839c5c82 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -17,6 +17,7 @@ export function mountImportGroupsApp(mountElement) {
jobsPath,
sourceUrl,
groupPathRegex,
+ groupUrlErrorMessage,
} = mountElement.dataset;
const apolloProvider = new VueApollo({
defaultClient: createApolloClient({
@@ -38,6 +39,7 @@ export function mountImportGroupsApp(mountElement) {
props: {
sourceUrl,
groupPathRegex: new RegExp(`^(${groupPathRegex})$`),
+ groupUrlErrorMessage,
},
});
},
diff --git a/app/assets/javascripts/import_entities/import_groups/utils.js b/app/assets/javascripts/import_entities/import_groups/utils.js
new file mode 100644
index 00000000000..b451008b6f9
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/utils.js
@@ -0,0 +1,13 @@
+import { NEW_NAME_FIELD } from './constants';
+
+export function isNameValid(group, validationRegex) {
+ return validationRegex.test(group.import_target[NEW_NAME_FIELD]);
+}
+
+export function getInvalidNameValidationMessage(group) {
+ return group.validation_errors.find(({ field }) => field === NEW_NAME_FIELD)?.message;
+}
+
+export function isInvalid(group, validationRegex) {
+ return Boolean(!isNameValid(group, validationRegex) || getInvalidNameValidationMessage(group));
+}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index e2fd608d9db..a97af5367fb 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -103,6 +103,7 @@ export default {
<tr
class="gl-h-11 gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100 gl-h-11"
data-qa-selector="project_import_row"
+ :data-qa-source-project="repo.importSource.fullName"
>
<td class="gl-p-4">
<gl-link :href="repo.importSource.providerLink" target="_blank" data-testid="providerLink"
@@ -155,7 +156,7 @@ export default {
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
- <td class="gl-p-4">
+ <td class="gl-p-4" data-qa-selector="import_status_indicator">
<import-status :status="importStatus" />
</td>
<td data-testid="actions">
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 3655f94f06f..1fd4083b920 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,6 +1,12 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
+import {
+ GlFormGroup,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormSelect,
+ GlFormTextarea,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
import eventHub from '../event_hub';
@@ -14,6 +20,9 @@ export default {
GlFormSelect,
GlFormTextarea,
},
+ directives: {
+ SafeHtml,
+ },
props: {
choices: {
type: Array,
@@ -122,6 +131,9 @@ export default {
this.validated = true;
},
},
+ helpHtmlConfig: {
+ ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
+ },
};
</script>
@@ -133,7 +145,7 @@ export default {
:state="valid"
>
<template #description>
- <span v-html="help"></span>
+ <span v-safe-html:[$options.helpHtmlConfig]="help"></span>
</template>
<template v-if="isCheckbox">
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 91f7c7dabf6..63f007170d0 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -86,7 +86,9 @@ export default {
},
},
helpHtmlConfig: {
+ ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
ADD_TAGS: ['use'], // to support icon SVGs
+ FORBID_ATTR: [], // This is trusted input so we can override the default config to allow data-* attributes
},
};
</script>
diff --git a/app/assets/javascripts/integrations/overrides/api.js b/app/assets/javascripts/integrations/overrides/api.js
new file mode 100644
index 00000000000..a379a864f9c
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/api.js
@@ -0,0 +1,10 @@
+import axios from '~/lib/utils/axios_utils';
+
+export const fetchOverrides = (overridesPath, { page, perPage }) => {
+ return axios.get(overridesPath, {
+ params: {
+ page,
+ per_page: perPage,
+ },
+ });
+};
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
new file mode 100644
index 00000000000..707ac946b98
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlLink, GlLoadingIcon, GlPagination, GlTable } from '@gitlab/ui';
+
+import { DEFAULT_PER_PAGE } from '~/api';
+import createFlash from '~/flash';
+import { fetchOverrides } from '~/integrations/overrides/api';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import { __, s__ } from '~/locale';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+
+export default {
+ name: 'IntegrationOverrides',
+ components: {
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
+ ProjectAvatar,
+ },
+ props: {
+ overridesPath: {
+ type: String,
+ required: true,
+ },
+ },
+ fields: [
+ {
+ key: 'name',
+ label: __('Project'),
+ },
+ ],
+ data() {
+ return {
+ isLoading: true,
+ overrides: [],
+ page: 1,
+ totalItems: 0,
+ };
+ },
+ computed: {
+ showPagination() {
+ return this.totalItems > this.$options.DEFAULT_PER_PAGE && this.overrides.length > 0;
+ },
+ },
+ mounted() {
+ this.loadOverrides();
+ },
+ methods: {
+ loadOverrides(page = this.page) {
+ this.isLoading = true;
+
+ fetchOverrides(this.overridesPath, {
+ page,
+ perPage: this.$options.DEFAULT_PER_PAGE,
+ })
+ .then(({ data, headers }) => {
+ const { page: newPage, total } = parseIntPagination(normalizeHeaders(headers));
+ this.page = newPage;
+ this.totalItems = total;
+ this.overrides = data;
+ })
+ .catch((error) => {
+ createFlash({
+ message: this.$options.i18n.defaultErrorMessage,
+ error,
+ captureError: true,
+ });
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ truncateNamespace,
+ },
+ DEFAULT_PER_PAGE,
+ i18n: {
+ defaultErrorMessage: s__(
+ 'Integrations|An error occurred while loading projects using custom settings.',
+ ),
+ tableEmptyText: s__('Integrations|There are no projects using custom settings'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-table
+ :items="overrides"
+ :fields="$options.fields"
+ :busy="isLoading"
+ show-empty
+ :empty-text="$options.i18n.tableEmptyText"
+ >
+ <template #cell(name)="{ item }">
+ <gl-link
+ class="gl-display-inline-flex gl-align-items-center gl-hover-text-decoration-none gl-text-body!"
+ :href="item.full_path"
+ >
+ <project-avatar
+ class="gl-mr-3"
+ :project-avatar-url="item.avatar_url"
+ :project-name="item.name"
+ aria-hidden="true"
+ />
+ {{ truncateNamespace(item.full_name) }} /&nbsp;
+
+ <strong>{{ item.name }}</strong>
+ </gl-link>
+ </template>
+
+ <template #table-busy>
+ <gl-loading-icon size="md" class="gl-my-2" />
+ </template>
+ </gl-table>
+ <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-pagination
+ v-if="showPagination"
+ :per-page="$options.DEFAULT_PER_PAGE"
+ :total-items="totalItems"
+ :value="page"
+ :disabled="isLoading"
+ @input="loadOverrides"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/overrides/index.js b/app/assets/javascripts/integrations/overrides/index.js
new file mode 100644
index 00000000000..0f03b23ba21
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import IntegrationOverrides from './components/integration_overrides.vue';
+
+export default () => {
+ const el = document.querySelector('.js-vue-integration-overrides');
+
+ if (!el) {
+ return null;
+ }
+
+ const { overridesPath } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(IntegrationOverrides, {
+ props: {
+ overridesPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue
index 2d1e57a1177..216078ed35e 100644
--- a/app/assets/javascripts/invite_members/components/group_select.vue
+++ b/app/assets/javascripts/invite_members/components/group_select.vue
@@ -111,6 +111,7 @@ export default {
data-testid="group-select-dropdown"
:text="selectedGroupName"
block
+ toggle-class="gl-mb-2"
menu-class="gl-w-full!"
>
<gl-search-box-by-type
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index 4aab1123af9..ab42e8cdfeb 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -9,13 +9,19 @@ import {
GlSprintf,
GlButton,
GlFormInput,
+ GlFormCheckboxGroup,
} from '@gitlab/ui';
import { partition, isString } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
-import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
-import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants';
+import {
+ INVITE_MEMBERS_IN_COMMENT,
+ GROUP_FILTERS,
+ USERS_FILTER_ALL,
+ MEMBER_AREAS_OF_FOCUS,
+} from '../constants';
import eventHub from '../event_hub';
import {
responseMessageFromError,
@@ -36,6 +42,7 @@ export default {
GlSprintf,
GlButton,
GlFormInput,
+ GlFormCheckboxGroup,
MembersTokenSelect,
GroupSelect,
},
@@ -70,10 +77,28 @@ export default {
required: false,
default: null,
},
+ usersFilter: {
+ type: String,
+ required: false,
+ default: USERS_FILTER_ALL,
+ },
+ filterId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
helpLink: {
type: String,
required: true,
},
+ areasOfFocusOptions: {
+ type: Array,
+ required: true,
+ },
+ noSelectionAreasOfFocus: {
+ type: Array,
+ required: true,
+ },
},
data() {
return {
@@ -83,9 +108,11 @@ export default {
inviteeType: 'members',
newUsersToInvite: [],
selectedDate: undefined,
+ selectedAreasOfFocus: [],
groupToBeSharedWith: {},
source: 'unknown',
invalidFeedbackMessage: '',
+ isLoading: false,
};
},
computed: {
@@ -127,10 +154,28 @@ export default {
this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0
);
},
+ areasOfFocusEnabled() {
+ return this.areasOfFocusOptions.length !== 0;
+ },
+ areasOfFocusForPost() {
+ if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) {
+ return this.noSelectionAreasOfFocus;
+ }
+
+ return this.selectedAreasOfFocus;
+ },
+ errorFieldDescription() {
+ if (this.inviteeType === 'group') {
+ return '';
+ }
+
+ return this.$options.labels[this.inviteeType].placeHolder;
+ },
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
+ this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view);
});
},
methods: {
@@ -151,9 +196,13 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
+ trackEvent(experimentName, eventName) {
+ const tracking = new ExperimentTracking(experimentName);
+ tracking.event(eventName);
+ },
closeModal() {
this.resetFields();
- this.$root.$emit(BV_HIDE_MODAL, this.modalId);
+ this.$refs.modal.hide();
},
sendInvite() {
if (this.isInviteGroup) {
@@ -164,16 +213,19 @@ export default {
},
trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
- const tracking = new ExperimentTracking(INVITE_MEMBERS_IN_COMMENT);
- tracking.event('comment_invite_success');
+ this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success');
}
+
+ this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit);
},
resetFields() {
+ this.isLoading = false;
this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined;
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
this.invalidFeedbackMessage = '';
+ this.selectedAreasOfFocus = [];
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
@@ -189,6 +241,7 @@ export default {
},
submitInviteMembers() {
this.invalidFeedbackMessage = '';
+ this.isLoading = true;
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const promises = [];
@@ -220,6 +273,7 @@ export default {
email: usersToInviteByEmail,
access_level: this.selectedAccessLevel,
invite_source: this.source,
+ areas_of_focus: this.areasOfFocusForPost,
};
},
addByUserIdPostData(usersToAddById) {
@@ -228,6 +282,7 @@ export default {
user_id: usersToAddById,
access_level: this.selectedAccessLevel,
invite_source: this.source,
+ areas_of_focus: this.areasOfFocusForPost,
};
},
shareWithGroupPostData(groupToBeSharedWith) {
@@ -247,12 +302,14 @@ export default {
}
this.invalidFeedbackMessage = message;
+ this.isLoading = false;
},
showToastMessageSuccess() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
this.closeModal();
},
showInvalidFeedbackMessage(response) {
+ this.isLoading = false;
this.invalidFeedbackMessage =
responseMessageFromError(response) || this.$options.labels.invalidFeedbackMessageDefault;
},
@@ -299,18 +356,24 @@ export default {
inviteButtonText: s__('InviteMembersModal|Invite'),
cancelButtonText: s__('InviteMembersModal|Cancel'),
headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
+ areasOfFocusLabel: s__(
+ 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
+ ),
},
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
<template>
<gl-modal
+ ref="modal"
:modal-id="modalId"
size="sm"
data-qa-selector="invite_members_modal_content"
:title="$options.labels[inviteeType].modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
+ @hidden="resetFields"
@close="resetFields"
+ @hide="resetFields"
>
<div>
<p ref="introText">
@@ -322,10 +385,9 @@ export default {
</p>
<gl-form-group
- class="gl-mt-2"
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
- :description="$options.labels[inviteeType].placeHolder"
+ :description="errorFieldDescription"
data-testid="members-form-group"
>
<label :id="$options.membersTokenSelectLabelId" class="col-form-label">{{
@@ -334,8 +396,11 @@ export default {
<members-token-select
v-if="!isInviteGroup"
v-model="newUsersToInvite"
+ class="gl-mb-2"
:validation-state="validationState"
:aria-labelledby="$options.membersTokenSelectLabelId"
+ :users-filter="usersFilter"
+ :filter-id="filterId"
@clear="handleMembersTokenSelectClear"
/>
<group-select
@@ -343,10 +408,11 @@ export default {
v-model="groupToBeSharedWith"
:groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId"
+ @input="handleMembersTokenSelectClear"
/>
</gl-form-group>
- <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label>
+ <label class="gl-font-weight-bold">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
@@ -376,7 +442,7 @@ export default {
</gl-sprintf>
</div>
- <label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{
+ <label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.labels.accessExpireDate
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
@@ -395,6 +461,16 @@ export default {
</template>
</gl-datepicker>
</div>
+ <div v-if="areasOfFocusEnabled">
+ <label class="gl-mt-5">
+ {{ $options.labels.areasOfFocusLabel }}
+ </label>
+ <gl-form-checkbox-group
+ v-model="selectedAreasOfFocus"
+ :options="areasOfFocusOptions"
+ data-testid="area-of-focus-checks"
+ />
+ </div>
</div>
<template #modal-footer>
@@ -405,6 +481,7 @@ export default {
<div class="gl-mr-3"></div>
<gl-button
:disabled="inviteDisabled"
+ :loading="isLoading"
variant="success"
data-qa-selector="invite_button"
data-testid="invite-button"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 7aece3b7bb4..e299e3f27b3 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -3,7 +3,7 @@ import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@
import { debounce } from 'lodash';
import { __ } from '~/locale';
import { getUsers } from '~/rest_api';
-import { SEARCH_DELAY } from '../constants';
+import { SEARCH_DELAY, USERS_FILTER_ALL, USERS_FILTER_SAML_PROVIDER_ID } from '../constants';
export default {
components: {
@@ -26,6 +26,16 @@ export default {
validationState: {
type: Boolean,
required: false,
+ default: false,
+ },
+ usersFilter: {
+ type: String,
+ required: false,
+ default: USERS_FILTER_ALL,
+ },
+ filterId: {
+ type: Number,
+ required: false,
default: null,
},
},
@@ -51,6 +61,15 @@ export default {
}
return '';
},
+ queryOptions() {
+ if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) {
+ return {
+ saml_provider_id: this.filterId,
+ ...this.$options.defaultQueryOptions,
+ };
+ }
+ return this.$options.defaultQueryOptions;
+ },
},
methods: {
handleTextInput(query) {
@@ -60,7 +79,7 @@ export default {
this.retrieveUsers(query);
},
retrieveUsers: debounce(function debouncedRetrieveUsers() {
- return getUsers(this.query, this.$options.queryOptions)
+ return getUsers(this.query, this.queryOptions)
.then((response) => {
this.users = response.data.map((token) => ({
id: token.id,
@@ -98,7 +117,7 @@ export default {
this.$emit('clear');
},
},
- queryOptions: { exclude_internal: true, active: true },
+ defaultQueryOptions: { exclude_internal: true, active: true },
i18n: {
inviteTextMessage: __('Invite "%{email}" by email'),
},
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 83e6cac0ac0..d7daf83e26b 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -3,6 +3,11 @@ import { __ } from '~/locale';
export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
+export const MEMBER_AREAS_OF_FOCUS = {
+ name: 'member_areas_of_focus',
+ view: 'view',
+ submit: 'submit',
+};
export const GROUP_FILTERS = {
ALL: 'all',
@@ -12,3 +17,5 @@ export const GROUP_FILTERS = {
export const API_MESSAGES = {
EMAIL_ALREADY_INVITED: __('Invite email has already been taken'),
};
+export const USERS_FILTER_ALL = 'all';
+export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 7501e9f4e6e..c1dfaa25dc7 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -23,6 +23,10 @@ export default function initInviteMembersModal() {
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
+ areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions),
+ noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus),
+ usersFilter: el.dataset.usersFilter,
+ filterId: parseInt(el.dataset.filterId, 10),
},
}),
});
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index fd9e3d5c916..5dc49d3ae15 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -51,15 +51,16 @@ export default class IssuableForm {
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
/* eslint-disable @gitlab/require-i18n-strings */
- this.wipRegex = new RegExp(
+ // prettier-ignore
+ this.draftRegex = new RegExp(
'^\\s*(' + // Line start, then any amount of leading whitespace
'draft\\s-\\s' + // Draft_-_ where "_" are *exactly* 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
+ '|draft:\\s*' + // Draft: and any following whitespace
+ '|draft\\s+' + // Draft_ 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
+ ')+' + // At least one repeated match of the preceding parenthetical
+ '\\s*', // Any amount of trailing whitespace
'i', // Match any case(s)
);
/* eslint-enable @gitlab/require-i18n-strings */
@@ -144,7 +145,7 @@ export default class IssuableForm {
}
workInProgress() {
- return this.wipRegex.test(this.titleField.val());
+ return this.draftRegex.test(this.titleField.val());
}
renderWipExplanation() {
@@ -170,7 +171,7 @@ export default class IssuableForm {
}
removeWip() {
- return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
+ return this.titleField.val(this.titleField.val().replace(this.draftRegex, ''));
}
addWip() {
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 20d1dce3905..29dd0b7fed5 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -205,7 +205,7 @@ export default {
>{{ issuableSymbol }}{{ issuable.iid }}</span
>
<span class="issuable-authored gl-display-none gl-sm-display-inline-block! gl-mr-3">
- &middot;
+ <span aria-hidden="true">&middot;</span>
<span
v-gl-tooltip:tooltipcontainer.bottom
data-testid="issuable-created-at"
@@ -229,17 +229,19 @@ export default {
</span>
<slot name="timeframe"></slot>
&nbsp;
- <gl-label
- v-for="(label, index) in labels"
- :key="index"
- :background-color="label.color"
- :title="labelTitle(label)"
- :description="label.description"
- :scoped="scopedLabel(label)"
- :target="labelTarget(label)"
- :class="{ 'gl-ml-2': index }"
- size="sm"
- />
+ <span v-if="labels.length" role="group" :aria-label="__('Labels')">
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="labelTitle(label)"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :target="labelTarget(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </span>
</div>
</div>
<div class="issuable-meta">
diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
index 977d03e62be..96b07031a11 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue
@@ -49,7 +49,7 @@ export default {
<span :title="tab.titleTooltip">{{ tab.title }}</span>
<gl-badge
v-if="tabCounts && isTabCountNumeric(tab)"
- variant="neutral"
+ variant="muted"
size="sm"
class="gl-tab-counter-badge"
>
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 9bfdbb41e23..35e7860cd9b 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -15,7 +15,7 @@ export default {
issuableTemplates: {
type: [Object, Array],
required: false,
- default: () => {},
+ default: () => ({}),
},
projectPath: {
type: String,
diff --git a/app/assets/javascripts/issue_show/components/fields/type.vue b/app/assets/javascripts/issue_show/components/fields/type.vue
index 1ed222531f4..3eac448c637 100644
--- a/app/assets/javascripts/issue_show/components/fields/type.vue
+++ b/app/assets/javascripts/issue_show/components/fields/type.vue
@@ -1,5 +1,5 @@
<script>
-import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
import { IssuableTypes } from '../../constants';
@@ -15,6 +15,7 @@ export default {
IssuableTypes,
components: {
GlFormGroup,
+ GlIcon,
GlDropdown,
GlDropdownItem,
},
@@ -72,6 +73,7 @@ export default {
is-check-item
@click="updateIssueType(type.value)"
>
+ <gl-icon :name="type.icon" />
{{ type.text }}
</gl-dropdown-item>
</gl-dropdown>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index bdaa8a4dd6b..001e8abb941 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -33,7 +33,7 @@ export default {
issuableTemplates: {
type: [Object, Array],
required: false,
- default: () => {},
+ default: () => [],
},
issuableType: {
type: String,
diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js
index d93f38c2ee1..64d39a79821 100644
--- a/app/assets/javascripts/issue_show/constants.js
+++ b/app/assets/javascripts/issue_show/constants.js
@@ -28,8 +28,8 @@ export const STATUS_PAGE_PUBLISHED = __('Published on status page');
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
export const IssuableTypes = [
- { value: 'issue', text: __('Issue') },
- { value: 'incident', text: __('Incident') },
+ { value: 'issue', text: __('Issue'), icon: 'issue-type-issue' },
+ { value: 'incident', text: __('Incident'), icon: 'issue-type-incident' },
];
export const IssueTypePath = 'issues';
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index b13a389b963..62b52afdaca 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -9,8 +9,7 @@ import { toNumber, omit } from 'lodash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
-// eslint-disable-next-line import/no-deprecated
-import { setUrlParams, urlParamsToObject, getParameterByName } from '~/lib/utils/url_utility';
+import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -264,8 +263,7 @@ export default {
});
},
getQueryObject() {
- // eslint-disable-next-line import/no-deprecated
- return urlParamsToObject(window.location.search);
+ return queryToObject(window.location.search, { gatherArrays: true });
},
onPaginate(newPage) {
if (newPage === this.page) return;
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 07492b0046c..a687a58a6ad 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -48,8 +48,8 @@ export default {
dueDate() {
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
},
- isDueDateInPast() {
- return isInPast(new Date(this.issue.dueDate));
+ showDueDateInRed() {
+ return isInPast(new Date(this.issue.dueDate)) && !this.issue.closedAt;
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
@@ -97,7 +97,7 @@ export default {
v-if="issue.dueDate"
v-gl-tooltip
class="issuable-due-date gl-display-none gl-sm-display-inline-block! gl-mr-3"
- :class="{ 'gl-text-red-500': isDueDateInPast }"
+ :class="{ 'gl-text-red-500': showDueDateInRed }"
:title="__('Due date')"
data-testid="issuable-due-date"
>
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 6563094ef72..ee0429c0432 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -9,10 +9,11 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { cloneDeep } from 'lodash';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
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';
@@ -20,7 +21,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
CREATED_DESC,
i18n,
- initialPageParams,
issuesCountSmartQueryBase,
MAX_LIST_SIZE,
PAGE_SIZE,
@@ -36,6 +36,7 @@ import {
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
urlSortParams,
@@ -46,12 +47,13 @@ import {
convertToUrlParams,
getDueDateValue,
getFilterTokens,
+ getInitialPageParams,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
@@ -63,6 +65,7 @@ import {
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_TYPE,
TOKEN_TITLE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
@@ -73,6 +76,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
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 reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchIterationsQuery from '../queries/search_iterations.query.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
@@ -160,18 +164,22 @@ export default {
},
},
data() {
+ const filterTokens = getFilterTokens(window.location.search);
const state = getParameterByName(PARAM_STATE);
+ const sortKey = getSortKey(getParameterByName(PARAM_SORT));
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ this.initialFilterTokens = cloneDeep(filterTokens);
+
return {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filterTokens: getFilterTokens(window.location.search),
+ filterTokens,
issues: [],
pageInfo: {},
- pageParams: initialPageParams,
+ pageParams: getInitialPageParams(sortKey),
showBulkEditSidebar: false,
- sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
+ sortKey: sortKey || defaultSortKey,
state: state || IssuableStates.Opened,
};
},
@@ -275,7 +283,6 @@ export default {
avatar_url: gon.current_user_avatar_url,
});
}
-
const tokens = [
{
type: TOKEN_TYPE_AUTHOR,
@@ -306,7 +313,6 @@ export default {
icon: 'clock',
token: MilestoneToken,
unique: true,
- defaultMilestones: [],
fetchMilestones: this.fetchMilestones,
},
{
@@ -317,6 +323,18 @@ export default {
defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
},
+ {
+ type: TOKEN_TYPE_TYPE,
+ title: TOKEN_TITLE_TYPE,
+ icon: 'issues',
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ options: [
+ { icon: 'issue-type-issue', title: 'issue', value: 'issue' },
+ { icon: 'issue-type-incident', title: 'incident', value: 'incident' },
+ { icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' },
+ ],
+ },
];
if (this.isSignedIn) {
@@ -518,12 +536,12 @@ export default {
},
handleClickTab(state) {
if (this.state !== state) {
- this.pageParams = initialPageParams;
+ this.pageParams = getInitialPageParams(this.sortKey);
}
this.state = state;
},
handleFilter(filter) {
- this.pageParams = initialPageParams;
+ this.pageParams = getInitialPageParams(this.sortKey);
this.filterTokens = filter;
},
handleNextPage() {
@@ -560,14 +578,16 @@ export default {
}
return axios
- .put(`${this.issuesPath}/${issueToMove.iid}/reorder`, {
- move_before_id: isMovingToBeginning ? null : moveBeforeId,
- move_after_id: isMovingToEnd ? null : moveAfterId,
+ .put(joinPaths(this.issuesPath, issueToMove.iid, 'reorder'), {
+ move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
+ move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
})
.then(() => {
- // Move issue to new position in list
- this.issues.splice(oldIndex, 1);
- this.issues.splice(newIndex, 0, issueToMove);
+ const serializedVariables = JSON.stringify(this.queryVariables);
+ this.$apollo.mutate({
+ mutation: reorderIssuesMutation,
+ variables: { oldIndex, newIndex, serializedVariables },
+ });
})
.catch(() => {
createFlash({ message: this.$options.i18n.reorderError });
@@ -575,7 +595,7 @@ export default {
},
handleSort(sortKey) {
if (this.sortKey !== sortKey) {
- this.pageParams = initialPageParams;
+ this.pageParams = getInitialPageParams(sortKey);
}
this.sortKey = sortKey;
},
@@ -593,7 +613,7 @@ export default {
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
- :initial-filter-value="filterTokens"
+ :initial-filter-value="initialFilterTokens"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
diff --git a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue
index ba0ca57523a..fb1dbef666c 100644
--- a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue
+++ b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue
@@ -59,9 +59,6 @@ export default {
shouldShowInProgressAlert: isInProgress(project.jiraImportStatus),
};
},
- skip() {
- return !this.isJiraConfigured || !this.canEdit;
- },
},
},
computed: {
@@ -75,6 +72,9 @@ export default {
labelTarget() {
return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`;
},
+ shouldRender() {
+ return this.jiraImport.shouldShowInProgressAlert || this.jiraImport.shouldShowFinishedAlert;
+ },
},
methods: {
hideFinishedAlert() {
@@ -89,7 +89,7 @@ export default {
</script>
<template>
- <div class="gl-my-5">
+ <div v-if="shouldRender" class="gl-my-5">
<gl-alert v-if="jiraImport.shouldShowInProgressAlert" @dismiss="hideInProgressAlert">
{{ __('Import in progress. Refresh page to see newly added issues.') }}
</gl-alert>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index d94d4b9a19a..3f5b0d1feb5 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -5,6 +5,8 @@ import {
FILTER_ANY,
FILTER_CURRENT,
FILTER_NONE,
+ FILTER_STARTED,
+ FILTER_UPCOMING,
OPERATOR_IS,
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -107,10 +109,14 @@ export const PARAM_DUE_DATE = 'due_date';
export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
-export const initialPageParams = {
+export const defaultPageSizeParams = {
firstPageSize: PAGE_SIZE,
};
+export const largePageSizeParams = {
+ firstPageSize: PAGE_SIZE_MANUAL,
+};
+
export const DUE_DATE_NONE = '0';
export const DUE_DATE_ANY = '';
export const DUE_DATE_OVERDUE = 'overdue';
@@ -186,12 +192,19 @@ 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 SPECIAL_FILTER_VALUES = [
+ FILTER_NONE,
+ FILTER_ANY,
+ FILTER_CURRENT,
+ FILTER_UPCOMING,
+ FILTER_STARTED,
+];
export const TOKEN_TYPE_AUTHOR = 'author_username';
export const TOKEN_TYPE_ASSIGNEE = 'assignee_username';
export const TOKEN_TYPE_MILESTONE = 'milestone';
export const TOKEN_TYPE_LABEL = 'labels';
+export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_ITERATION = 'iteration';
@@ -231,10 +244,12 @@ export const filters = {
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'milestoneTitle',
+ [SPECIAL_FILTER]: 'milestoneWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'milestone_title',
+ [SPECIAL_FILTER]: 'milestone_title',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
@@ -256,6 +271,18 @@ export const filters = {
},
},
},
+ [TOKEN_TYPE_TYPE]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'types',
+ [SPECIAL_FILTER]: 'types',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'type[]',
+ [SPECIAL_FILTER]: 'type[]',
+ },
+ },
+ },
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'myReactionEmoji',
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 71ceb9bef55..dcc7ee72273 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -1,5 +1,7 @@
+import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
@@ -13,9 +15,16 @@ export function mountJiraIssuesListApp() {
return false;
}
- Vue.use(VueApollo);
+ const { issuesPath, projectPath } = el.dataset;
+ const canEdit = parseBoolean(el.dataset.canEdit);
+ const isJiraConfigured = parseBoolean(el.dataset.isJiraConfigured);
+
+ if (!isJiraConfigured || !canEdit) {
+ return false;
+ }
- const defaultClient = createDefaultClient();
+ Vue.use(VueApollo);
+ const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
@@ -26,10 +35,10 @@ export function mountJiraIssuesListApp() {
render(createComponent) {
return createComponent(JiraIssuesImportStatusRoot, {
props: {
- canEdit: parseBoolean(el.dataset.canEdit),
- isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
- issuesPath: el.dataset.issuesPath,
- projectPath: el.dataset.projectPath,
+ canEdit,
+ isJiraConfigured,
+ issuesPath,
+ projectPath,
},
});
},
@@ -74,7 +83,27 @@ export function mountIssuesListApp() {
Vue.use(VueApollo);
- const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+ const resolvers = {
+ Mutation: {
+ reorderIssues: (_, { oldIndex, newIndex, serializedVariables }, { cache }) => {
+ const variables = JSON.parse(serializedVariables);
+ const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
+
+ const data = produce(sourceData, (draftData) => {
+ const issues = draftData.project.issues.nodes.slice();
+ const issueToMove = issues[oldIndex];
+ issues.splice(oldIndex, 1);
+ issues.splice(newIndex, 0, issueToMove);
+
+ draftData.project.issues.nodes = issues;
+ });
+
+ cache.writeQuery({ query: getIssuesQuery, variables, data });
+ },
+ },
+ };
+
+ const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true });
const apolloProvider = new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
index 124190915c0..30a01b4c3b0 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -12,6 +12,8 @@ query getProjectIssues(
$authorUsername: String
$labelName: [String]
$milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $types: [IssueType!]
$not: NegatedIssueFilterInput
$beforeCursor: String
$afterCursor: String
@@ -28,6 +30,8 @@ query getProjectIssues(
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
not: $not
before: $beforeCursor
after: $afterCursor
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
index a1742859640..e6896131da9 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
@@ -7,6 +7,8 @@ query getProjectIssuesCount(
$authorUsername: String
$labelName: [String]
$milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $types: [IssueType!]
$not: NegatedIssueFilterInput
) {
project(fullPath: $projectPath) {
@@ -18,6 +20,8 @@ query getProjectIssuesCount(
authorUsername: $authorUsername
labelName: $labelName
milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
not: $not
) {
count
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
index f7ebf64ffb8..633b06eced8 100644
--- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -7,6 +7,7 @@ fragment IssueFragment on Issue {
downvotes
dueDate
humanTimeEstimate
+ mergeRequestsCount
moved
title
updatedAt
diff --git a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
new file mode 100644
index 00000000000..5927e3e83c7
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
@@ -0,0 +1,7 @@
+mutation reorderIssues($oldIndex: Int, $newIndex: Int, $serializedVariables: String) {
+ reorderIssues(
+ oldIndex: $oldIndex
+ newIndex: $newIndex
+ serializedVariables: $serializedVariables
+ ) @client
+}
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
index 49f937cc453..1d3d07475af 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -3,12 +3,14 @@ import {
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
+ defaultPageSizeParams,
DUE_DATE_ASC,
DUE_DATE_DESC,
DUE_DATE_VALUES,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
+ largePageSizeParams,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
NORMAL_FILTER,
@@ -21,6 +23,8 @@ import {
SPECIAL_FILTER_VALUES,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_TYPE,
UPDATED_ASC,
UPDATED_DESC,
URL_PARAM,
@@ -35,6 +39,9 @@ import {
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
+export const getInitialPageParams = (sortKey) =>
+ sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams;
+
export const getSortKey = (sort) =>
Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
@@ -186,8 +193,17 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
-const isIterationSpecialValue = (tokenType, value) =>
- tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
+const isWildcardValue = (tokenType, value) =>
+ (tokenType === TOKEN_TYPE_ITERATION || tokenType === TOKEN_TYPE_MILESTONE) &&
+ SPECIAL_FILTER_VALUES.includes(value);
+
+const requiresUpperCaseValue = (tokenType, value) =>
+ tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
+
+const formatData = (token) =>
+ requiresUpperCaseValue(token.type, token.value.data)
+ ? token.value.data.toUpperCase()
+ : token.value.data;
export const convertToApiParams = (filterTokens) => {
const params = {};
@@ -199,9 +215,7 @@ export const convertToApiParams = (filterTokens) => {
const filterType = getFilterType(token.value.data, token.type);
const field = filters[token.type][API_PARAM][filterType];
const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
- const data = isIterationSpecialValue(token.type, token.value.data)
- ? token.value.data.toUpperCase()
- : token.value.data;
+ const data = formatData(token);
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
diff --git a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
new file mode 100644
index 00000000000..66fcb8e10eb
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
@@ -0,0 +1,174 @@
+<script>
+import { GlFormGroup, GlButton, GlFormInput, GlForm, GlAlert } from '@gitlab/ui';
+import {
+ CREATE_BRANCH_ERROR_GENERIC,
+ CREATE_BRANCH_ERROR_WITH_CONTEXT,
+ I18N_NEW_BRANCH_LABEL_DROPDOWN,
+ I18N_NEW_BRANCH_LABEL_BRANCH,
+ I18N_NEW_BRANCH_LABEL_SOURCE,
+ I18N_NEW_BRANCH_SUBMIT_BUTTON_TEXT,
+} from '../constants';
+import createBranchMutation from '../graphql/mutations/create_branch.mutation.graphql';
+import ProjectDropdown from './project_dropdown.vue';
+import SourceBranchDropdown from './source_branch_dropdown.vue';
+
+const DEFAULT_ALERT_VARIANT = 'danger';
+const DEFAULT_ALERT_PARAMS = {
+ title: '',
+ message: '',
+ variant: DEFAULT_ALERT_VARIANT,
+};
+
+export default {
+ name: 'JiraConnectNewBranch',
+ components: {
+ GlFormGroup,
+ GlButton,
+ GlFormInput,
+ GlForm,
+ GlAlert,
+ ProjectDropdown,
+ SourceBranchDropdown,
+ },
+ inject: ['initialBranchName'],
+ data() {
+ return {
+ selectedProject: null,
+ selectedSourceBranchName: null,
+ branchName: this.initialBranchName,
+ createBranchLoading: false,
+ alertParams: {
+ ...DEFAULT_ALERT_PARAMS,
+ },
+ };
+ },
+ computed: {
+ selectedProjectId() {
+ return this.selectedProject?.id;
+ },
+ showAlert() {
+ return Boolean(this.alertParams?.message);
+ },
+ disableSubmitButton() {
+ return !(this.selectedProject && this.selectedSourceBranchName && this.branchName);
+ },
+ },
+ methods: {
+ displayAlert({ title, message, variant = DEFAULT_ALERT_VARIANT } = {}) {
+ this.alertParams = {
+ title,
+ message,
+ variant,
+ };
+ },
+ onAlertDismiss() {
+ this.alertParams = {
+ ...DEFAULT_ALERT_PARAMS,
+ };
+ },
+ onProjectSelect(project) {
+ this.selectedProject = project;
+ this.selectedSourceBranchName = null; // reset branch selection
+ },
+ onSourceBranchSelect(branchName) {
+ this.selectedSourceBranchName = branchName;
+ },
+ onError({ title, message } = {}) {
+ this.displayAlert({
+ message,
+ title,
+ });
+ },
+ onSubmit() {
+ this.createBranch();
+ },
+ async createBranch() {
+ this.createBranchLoading = true;
+
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: createBranchMutation,
+ variables: {
+ name: this.branchName,
+ ref: this.selectedSourceBranchName,
+ projectPath: this.selectedProject.fullPath,
+ },
+ });
+ const { errors } = data.createBranch;
+ if (errors.length > 0) {
+ this.onError({
+ title: CREATE_BRANCH_ERROR_WITH_CONTEXT,
+ message: errors[0],
+ });
+ } else {
+ this.$emit('success');
+ }
+ } catch (e) {
+ this.onError({
+ message: CREATE_BRANCH_ERROR_GENERIC,
+ });
+ }
+
+ this.createBranchLoading = false;
+ },
+ },
+ i18n: {
+ I18N_NEW_BRANCH_LABEL_DROPDOWN,
+ I18N_NEW_BRANCH_LABEL_BRANCH,
+ I18N_NEW_BRANCH_LABEL_SOURCE,
+ I18N_NEW_BRANCH_SUBMIT_BUTTON_TEXT,
+ },
+};
+</script>
+<template>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-alert
+ v-if="showAlert"
+ class="gl-mb-5"
+ :variant="alertParams.variant"
+ :title="alertParams.title"
+ @dismiss="onAlertDismiss"
+ >
+ {{ alertParams.message }}
+ </gl-alert>
+ <gl-form-group :label="$options.i18n.I18N_NEW_BRANCH_LABEL_DROPDOWN" label-for="project-select">
+ <project-dropdown
+ id="project-select"
+ :selected-project="selectedProject"
+ @change="onProjectSelect"
+ @error="onError"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.i18n.I18N_NEW_BRANCH_LABEL_BRANCH"
+ label-for="branch-name-input"
+ >
+ <gl-form-input id="branch-name-input" v-model="branchName" type="text" required />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.i18n.I18N_NEW_BRANCH_LABEL_SOURCE"
+ label-for="source-branch-select"
+ >
+ <source-branch-dropdown
+ id="source-branch-select"
+ :selected-project="selectedProject"
+ :selected-branch-name="selectedSourceBranchName"
+ @change="onSourceBranchSelect"
+ @error="onError"
+ />
+ </gl-form-group>
+
+ <div class="form-actions">
+ <gl-button
+ :loading="createBranchLoading"
+ type="submit"
+ variant="confirm"
+ :disabled="disableSubmitButton"
+ >
+ {{ $options.i18n.I18N_NEW_BRANCH_SUBMIT_BUTTON_TEXT }}
+ </gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
index c1f57be7f97..751f3e9639d 100644
--- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -41,7 +41,7 @@ export default {
};
},
update(data) {
- return data?.projects?.nodes.filter((project) => !project.repository.empty) ?? [];
+ return data?.projects?.nodes.filter((project) => !project.repository?.empty) ?? [];
},
result() {
this.initialProjectsLoading = false;
@@ -60,7 +60,7 @@ export default {
},
},
methods: {
- async onProjectSelect(project) {
+ onProjectSelect(project) {
this.$emit('change', project);
},
onError({ message } = {}) {
diff --git a/app/assets/javascripts/jira_connect/branches/constants.js b/app/assets/javascripts/jira_connect/branches/constants.js
index 987c8d356b4..ab9d3b2c110 100644
--- a/app/assets/javascripts/jira_connect/branches/constants.js
+++ b/app/assets/javascripts/jira_connect/branches/constants.js
@@ -1,2 +1,25 @@
+import { __, s__ } from '~/locale';
+
export const BRANCHES_PER_PAGE = 20;
export const PROJECTS_PER_PAGE = 20;
+
+export const I18N_NEW_BRANCH_LABEL_DROPDOWN = __('Project');
+export const I18N_NEW_BRANCH_LABEL_BRANCH = __('Branch name');
+export const I18N_NEW_BRANCH_LABEL_SOURCE = __('Source branch');
+export const I18N_NEW_BRANCH_SUBMIT_BUTTON_TEXT = __('Create branch');
+
+export const CREATE_BRANCH_ERROR_GENERIC = s__(
+ 'JiraConnect|Failed to create branch. Please try again.',
+);
+export const CREATE_BRANCH_ERROR_WITH_CONTEXT = s__('JiraConnect|Failed to create branch.');
+
+export const I18N_PAGE_TITLE_WITH_BRANCH_NAME = s__(
+ 'JiraConnect|Create branch for Jira issue %{jiraIssue}',
+);
+export const I18N_PAGE_TITLE_DEFAULT = __('New branch');
+export const I18N_NEW_BRANCH_SUCCESS_TITLE = s__(
+ 'JiraConnect|New branch was successfully created.',
+);
+export const I18N_NEW_BRANCH_SUCCESS_MESSAGE = s__(
+ 'JiraConnect|You can now close this window and return to Jira.',
+);
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql b/app/assets/javascripts/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql
new file mode 100644
index 00000000000..7e9cbda8317
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/graphql/mutations/create_branch.mutation.graphql
@@ -0,0 +1,6 @@
+mutation createBranch($name: String!, $projectPath: ID!, $ref: String!) {
+ createBranch(input: { name: $name, projectPath: $projectPath, ref: $ref }) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
new file mode 100644
index 00000000000..95bd4f5c675
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import JiraConnectNewBranchPage from '~/jira_connect/branches/pages/index.vue';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export default async function initJiraConnectBranches() {
+ const el = document.querySelector('.js-jira-connect-create-branch');
+ if (!el) {
+ return null;
+ }
+
+ const { initialBranchName, successStateSvgPath } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ initialBranchName,
+ successStateSvgPath,
+ },
+ render(createElement) {
+ return createElement(JiraConnectNewBranchPage);
+ },
+ });
+}
diff --git a/app/assets/javascripts/jira_connect/branches/pages/index.vue b/app/assets/javascripts/jira_connect/branches/pages/index.vue
new file mode 100644
index 00000000000..d72dec6cdee
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/pages/index.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import NewBranchForm from '../components/new_branch_form.vue';
+import {
+ I18N_PAGE_TITLE_WITH_BRANCH_NAME,
+ I18N_PAGE_TITLE_DEFAULT,
+ I18N_NEW_BRANCH_SUCCESS_TITLE,
+ I18N_NEW_BRANCH_SUCCESS_MESSAGE,
+} from '../constants';
+
+export default {
+ components: {
+ GlEmptyState,
+ NewBranchForm,
+ },
+ inject: ['initialBranchName', 'successStateSvgPath'],
+ data() {
+ return {
+ showForm: true,
+ };
+ },
+ computed: {
+ pageTitle() {
+ return this.initialBranchName
+ ? sprintf(this.$options.i18n.I18N_PAGE_TITLE_WITH_BRANCH_NAME, {
+ jiraIssue: this.initialBranchName,
+ })
+ : this.$options.i18n.I18N_PAGE_TITLE_DEFAULT;
+ },
+ },
+ methods: {
+ onNewBranchFormSuccess() {
+ // light-weight toggle to hide the form and show the success state
+ this.showForm = false;
+ },
+ },
+ i18n: {
+ I18N_PAGE_TITLE_WITH_BRANCH_NAME,
+ I18N_PAGE_TITLE_DEFAULT,
+ I18N_NEW_BRANCH_SUCCESS_TITLE,
+ I18N_NEW_BRANCH_SUCCESS_MESSAGE,
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="gl-border-1 gl-border-b-solid gl-border-gray-100 gl-mb-5 gl-mt-7">
+ <h1 data-testid="page-title" class="page-title">{{ pageTitle }}</h1>
+ </div>
+
+ <new-branch-form v-if="showForm" @success="onNewBranchFormSuccess" />
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.I18N_NEW_BRANCH_SUCCESS_TITLE"
+ :description="$options.i18n.I18N_NEW_BRANCH_SUCCESS_MESSAGE"
+ :svg-path="successStateSvgPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/.eslintrc.yml b/app/assets/javascripts/jira_connect/subscriptions/.eslintrc.yml
index 053f8c6b285..053f8c6b285 100644
--- a/app/assets/javascripts/jira_connect/.eslintrc.yml
+++ b/app/assets/javascripts/jira_connect/subscriptions/.eslintrc.yml
diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/subscriptions/api.js
index abf2c070e68..14947b6c835 100644
--- a/app/assets/javascripts/jira_connect/api.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/api.js
@@ -1,5 +1,5 @@
import axios from 'axios';
-import { getJwt } from '~/jira_connect/utils';
+import { getJwt } from './utils';
export const addSubscription = async (addPath, namespace) => {
const jwt = await getJwt();
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index ff4dfb23687..413424be28d 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlButton, GlLink, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapState, mapMutations } from 'vuex';
-import { retrieveAlert, getLocation } from '~/jira_connect/utils';
+import { retrieveAlert, getLocation } from '~/jira_connect/subscriptions/utils';
import { __ } from '~/locale';
import { SET_ALERT } from '../store/mutation_types';
import GroupsList from './groups_list.vue';
diff --git a/app/assets/javascripts/jira_connect/components/group_item_name.vue b/app/assets/javascripts/jira_connect/subscriptions/components/group_item_name.vue
index e6c172dae9e..e6c172dae9e 100644
--- a/app/assets/javascripts/jira_connect/components/group_item_name.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/group_item_name.vue
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue
index 55233bb6326..5a49d7c1a90 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list.vue
@@ -1,7 +1,10 @@
<script>
import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
-import { fetchGroups } from '~/jira_connect/api';
-import { DEFAULT_GROUPS_PER_PAGE, MINIMUM_SEARCH_TERM_LENGTH } from '~/jira_connect/constants';
+import { fetchGroups } from '~/jira_connect/subscriptions/api';
+import {
+ DEFAULT_GROUPS_PER_PAGE,
+ MINIMUM_SEARCH_TERM_LENGTH,
+} from '~/jira_connect/subscriptions/constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import GroupsListItem from './groups_list_item.vue';
diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
index ad046920dd1..ed7585e8a88 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/groups_list_item.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { addSubscription } from '~/jira_connect/api';
-import { persistAlert, reloadPage } from '~/jira_connect/utils';
+import { addSubscription } from '~/jira_connect/subscriptions/api';
+import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
import { s__ } from '~/locale';
import GroupItemName from './group_item_name.vue';
diff --git a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
index a606e2edbbb..7062fb370ed 100644
--- a/app/assets/javascripts/jira_connect/components/subscriptions_list.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue
@@ -2,8 +2,8 @@
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapMutations } from 'vuex';
-import { removeSubscription } from '~/jira_connect/api';
-import { reloadPage } from '~/jira_connect/utils';
+import { removeSubscription } from '~/jira_connect/subscriptions/api';
+import { reloadPage } from '~/jira_connect/subscriptions/utils';
import { __, s__ } from '~/locale';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { SET_ALERT } from '../store/mutation_types';
diff --git a/app/assets/javascripts/jira_connect/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 8dff83eabb5..8dff83eabb5 100644
--- a/app/assets/javascripts/jira_connect/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js
index bc0d21c6c9a..f1262be0174 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/index.js
@@ -1,13 +1,13 @@
-import '../webpack';
+import '../../webpack';
import setConfigs from '@gitlab/ui/dist/config';
import Vue from 'vue';
-import { getLocation, sizeToParent } from '~/jira_connect/utils';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
+import { getLocation, sizeToParent } from './utils';
const store = createStore();
diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/subscriptions/store/index.js
index de830e3891a..de830e3891a 100644
--- a/app/assets/javascripts/jira_connect/store/index.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/index.js
diff --git a/app/assets/javascripts/jira_connect/store/mutation_types.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js
index 15f36b824d9..15f36b824d9 100644
--- a/app/assets/javascripts/jira_connect/store/mutation_types.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js
diff --git a/app/assets/javascripts/jira_connect/store/mutations.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js
index 2a25e0fe25f..2a25e0fe25f 100644
--- a/app/assets/javascripts/jira_connect/store/mutations.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js
diff --git a/app/assets/javascripts/jira_connect/store/state.js b/app/assets/javascripts/jira_connect/subscriptions/store/state.js
index c807df03f00..c807df03f00 100644
--- a/app/assets/javascripts/jira_connect/store/state.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/store/state.js
diff --git a/app/assets/javascripts/jira_connect/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js
index ecd1a31339a..ecd1a31339a 100644
--- a/app/assets/javascripts/jira_connect/utils.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index a6eff743ce9..d90377029c5 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -46,7 +46,7 @@ export default {
return timeIntervalInWords(this.job.queued);
},
runnerHelpUrl() {
- return helpPagePath('ci/runners/index.html', {
+ return helpPagePath('ci/runners/configure_runners.html', {
anchor: 'set-maximum-job-timeout-for-a-runner',
});
},
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 36b0ad43b14..1780afd39e8 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -2,10 +2,12 @@
import { GlLink, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
CiIcon,
+ clipboardButton,
GlDropdown,
GlDropdownItem,
GlLink,
@@ -45,7 +47,7 @@ export default {
<template>
<div class="dropdown">
<div class="js-pipeline-info" data-testid="pipeline-info">
- <ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
+ <ci-icon :status="pipeline.details.status" />
<span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span>
<gl-link
@@ -85,7 +87,14 @@ export default {
</template>
<gl-link v-else :href="pipeline.ref.path" class="link-commit ref-name">{{
pipeline.ref.name
- }}</gl-link>
+ }}</gl-link
+ ><clipboard-button
+ :text="pipeline.ref.name"
+ :title="__('Copy reference')"
+ category="tertiary"
+ size="small"
+ data-testid="copy-source-ref-link"
+ />
</template>
</div>
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index a8be5d8d039..53e3dbbad0d 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -13,6 +13,7 @@ import {
scrollUp,
} from '~/lib/utils/scroll_utils';
import { __ } from '~/locale';
+import { reportToSentry } from '../utils';
import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
@@ -175,11 +176,14 @@ export const fetchTrace = ({ dispatch, state }) =>
dispatch('startPollingTrace');
}
})
- .catch((e) =>
- e.response.status === httpStatusCodes.FORBIDDEN
- ? dispatch('receiveTraceUnauthorizedError')
- : dispatch('receiveTraceError'),
- );
+ .catch((e) => {
+ if (e.response.status === httpStatusCodes.FORBIDDEN) {
+ dispatch('receiveTraceUnauthorizedError');
+ } else {
+ reportToSentry('job_actions', e);
+ dispatch('receiveTraceError');
+ }
+ });
export const startPollingTrace = ({ dispatch, commit }) => {
const traceTimeout = setTimeout(() => {
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 36391a4d433..b64734e29f6 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -132,7 +132,7 @@ export const logLinesParserLegacy = (lines = [], accumulator = []) =>
);
export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLines = []) => {
- let currentLine = previousTraceState?.prevLineCount ?? 0;
+ let currentLineCount = previousTraceState?.prevLineCount ?? 0;
let currentHeader = previousTraceState?.currentHeader;
let isPreviousLineHeader = previousTraceState?.isPreviousLineHeader ?? false;
const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : [];
@@ -141,27 +141,27 @@ export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLi
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
// First run we can use the current index, later runs we have to retrieve the last number of lines
- currentLine = previousTraceState?.prevLineCount ? currentLine + 1 : i + 1;
+ currentLineCount = previousTraceState?.prevLineCount ? currentLineCount + 1 : i + 1;
if (line.section_header && !isPreviousLineHeader) {
// If there's no previous line header that means we're at the root of the log
isPreviousLineHeader = true;
- parsedLines.push(parseHeaderLine(line, currentLine));
+ parsedLines.push(parseHeaderLine(line, currentLineCount));
currentHeader = { index: parsedLines.length - 1 };
} else if (line.section_header && isPreviousLineHeader) {
// If there's a current section, we can't push to the parsedLines array
sectionsQueue.push(currentHeader);
- currentHeader = parseHeaderLine(line, currentLine); // Let's parse the incoming header line
+ currentHeader = parseHeaderLine(line, currentLineCount); // Let's parse the incoming header line
} else if (line.section && !line.section_duration) {
// We're inside a collapsible section and want to parse a standard line
if (currentHeader?.index) {
// If the current section header is only an index, add the line as part of the lines
// array of the current collapsible section
- parsedLines[currentHeader.index].lines.push(parseLine(line, currentLine));
+ parsedLines[currentHeader.index].lines.push(parseLine(line, currentLineCount));
} else {
// Otherwise add it to the innermost collapsible section lines array
- currentHeader.lines.push(parseLine(line, currentLine));
+ currentHeader.lines.push(parseLine(line, currentLineCount));
}
} else if (line.section && line.section_duration) {
// NOTE: This marks the end of a section_header
@@ -174,7 +174,7 @@ export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLi
parsedLines[currentHeader.index].line.section_duration = line.section_duration;
isPreviousLineHeader = false;
currentHeader = null;
- } else {
+ } else if (currentHeader?.isHeader) {
currentHeader.line.section_duration = line.section_duration;
if (previousSection && previousSection?.index) {
@@ -185,9 +185,14 @@ export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLi
}
currentHeader = previousSection;
+ } else {
+ // On older job logs, there's no `section_header: true` response, it's just an object
+ // with the `section_duration` and `section` props, so we just parse it
+ // as a standard line
+ parsedLines.push(parseLine(line, currentLineCount));
}
} else {
- parsedLines.push(parseLine(line, currentLine));
+ parsedLines.push(parseLine(line, currentLineCount));
}
}
@@ -197,7 +202,7 @@ export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLi
isPreviousLineHeader,
currentHeader,
sectionsQueue,
- prevLineCount: lines.length,
+ prevLineCount: currentLineCount,
},
};
};
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index 1ccecf3eb53..bb27658369f 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -1,3 +1,5 @@
+import * as Sentry from '@sentry/browser';
+
/**
* capture anything starting with http:// or https://
* https?:\/\/
@@ -10,3 +12,10 @@
*/
export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
export default { linkRegex };
+
+export const reportToSentry = (component, failureType) => {
+ Sentry.withScope((scope) => {
+ scope.setTag('component', component);
+ Sentry.captureException(failureType);
+ });
+};
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index b7cb6aa0a21..2b4dd205cf1 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -44,7 +44,7 @@ export default class LazyLoader {
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
- if (contentNode) {
+ if (contentNode && !this.mutationObserver) {
this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
this.mutationObserver.observe(contentNode, {
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 4357918672d..a026f76e51b 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -1,14 +1,14 @@
import { sanitize as dompurifySanitize, addHook } from 'dompurify';
import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
-// Safely allow SVG <use> tags
-
const defaultConfig = {
+ // Safely allow SVG <use> tags
ADD_TAGS: ['use'],
+ // Prevent possible XSS attacks with data-* attributes used by @rails/ujs
+ // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
+ FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
};
-const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
-
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
@@ -46,19 +46,10 @@ const sanitizeSvgIcon = (node) => {
removeUnsafeHref(node, 'xlink:href');
};
-const sanitizeHTMLAttributes = (node) => {
- forbiddenDataAttrs.forEach((attr) => {
- if (node.hasAttribute(attr)) {
- node.removeAttribute(attr);
- }
- });
-};
-
addHook('afterSanitizeAttributes', (node) => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
- sanitizeHTMLAttributes(node);
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8a051041fbe..8f86fd55d6e 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -151,11 +151,24 @@ export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = (e) => e.metaKey || e.ctrlKey || e.which === 2;
+/**
+ * Get the current computed outer height for given selector.
+ */
+export const getOuterHeight = (selector) => {
+ const element = document.querySelector(selector);
+
+ if (!element) {
+ return undefined;
+ }
+
+ return element.offsetHeight;
+};
+
export const contentTop = () => {
const isDesktop = breakpointInstance.isDesktop();
const heightCalculators = [
- () => $('#js-peek').outerHeight(),
- () => $('.navbar-gitlab').outerHeight(),
+ () => getOuterHeight('#js-peek'),
+ () => getOuterHeight('.navbar-gitlab'),
({ desktop }) => {
const container = document.querySelector('.line-resolve-all-container');
let size = 0;
@@ -166,14 +179,14 @@ export const contentTop = () => {
return size;
},
- () => $('.merge-request-tabs').outerHeight(),
- () => $('.js-diff-files-changed').outerHeight(),
+ () => getOuterHeight('.merge-request-tabs'),
+ () => getOuterHeight('.js-diff-files-changed'),
({ desktop }) => {
const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
let size;
if (desktop && diffsTabIsActive) {
- size = $('.diff-file .file-title-flex-parent:visible').outerHeight();
+ size = getOuterHeight('.diff-file .file-title-flex-parent:not([style="display:none"])');
}
return size;
@@ -182,7 +195,7 @@ export const contentTop = () => {
let size;
if (desktop) {
- size = $('.mr-version-controls').outerHeight();
+ size = getOuterHeight('.mr-version-controls');
}
return size;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 7922ff22a70..e9772232eaf 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -474,19 +474,17 @@ export function queryToObject(query, { gatherArrays = false, legacySpacesDecode
}
const decodedValue = legacySpacesDecode ? decodeURIComponent(value) : decodeUrlParameter(value);
+ const decodedKey = legacySpacesDecode ? decodeURIComponent(key) : decodeUrlParameter(key);
- if (gatherArrays && key.endsWith('[]')) {
- const decodedKey = legacySpacesDecode
- ? decodeURIComponent(key.slice(0, -2))
- : decodeUrlParameter(key.slice(0, -2));
+ if (gatherArrays && decodedKey.endsWith('[]')) {
+ const decodedArrayKey = decodedKey.slice(0, -2);
- if (!Array.isArray(accumulator[decodedKey])) {
- accumulator[decodedKey] = [];
+ if (!Array.isArray(accumulator[decodedArrayKey])) {
+ accumulator[decodedArrayKey] = [];
}
- accumulator[decodedKey].push(decodedValue);
- } else {
- const decodedKey = legacySpacesDecode ? decodeURIComponent(key) : decodeUrlParameter(key);
+ accumulator[decodedArrayKey].push(decodedValue);
+ } else {
accumulator[decodedKey] = decodedValue;
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5c14000a2aa..1aaefcaa13b 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -27,7 +27,6 @@ import { getLocationHash, visitUrl } from './lib/utils/url_utility';
import initFeatureHighlight from './feature_highlight';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
-import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
@@ -36,7 +35,6 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
-import navEventHub, { EVENT_RESPONSIVE_TOGGLE } from './nav/event_hub';
import 'ee_else_ce/main_ee';
@@ -92,7 +90,6 @@ function deferredInitialisation() {
initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
- initFrequentItemDropdowns();
initPersistentUserCallouts();
initDefaultTrackers();
initFeatureHighlight();
@@ -133,138 +130,132 @@ function deferredInitialisation() {
setTimeout(() => $body.addClass('page-initialised'), 1000);
}
-document.addEventListener('DOMContentLoaded', () => {
- const $body = $('body');
- const $document = $(document);
- const bootstrapBreakpoint = bp.getBreakpointSize();
-
- initUserTracking();
- initLayoutNav();
- initAlertHandler();
-
- // Set the default path for all cookies to GitLab's root directory
- Cookies.defaults.path = gon.relative_url_root || '/';
-
- // `hashchange` is not triggered when link target is already in window.location
- $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() {
- const href = this.getAttribute('href');
- if (href.substr(1) === getLocationHash()) {
- setTimeout(handleLocationHash, 1);
- }
- });
+const $body = $('body');
+const $document = $(document);
+const bootstrapBreakpoint = bp.getBreakpointSize();
+
+initUserTracking();
+initLayoutNav();
+initAlertHandler();
+
+// Set the default path for all cookies to GitLab's root directory
+Cookies.defaults.path = gon.relative_url_root || '/';
- /**
- * TODO: Apparently we are collapsing the right sidebar on certain screensizes per default
- * except on issue board pages. Why can't we do it with CSS?
- *
- * Proposal: Expose a global sidebar API, which we could import wherever we are manipulating
- * the visibility of the sidebar.
- *
- * Quick fix: Get rid of jQuery for this implementation
- */
- const isBoardsPage = /(projects|groups):boards:show/.test(document.body.dataset.page);
- if (!isBoardsPage && (bootstrapBreakpoint === 'sm' || bootstrapBreakpoint === 'xs')) {
- const $rightSidebar = $('aside.right-sidebar');
- const $layoutPage = $('.layout-page');
-
- if ($rightSidebar.length > 0) {
- $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- $layoutPage.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- $layoutPage.removeClass('right-sidebar-expanded right-sidebar-collapsed');
- }
+// `hashchange` is not triggered when link target is already in window.location
+$body.on('click', 'a[href^="#"]', function clickHashLinkCallback() {
+ const href = this.getAttribute('href');
+ if (href.substr(1) === getLocationHash()) {
+ setTimeout(handleLocationHash, 1);
}
+});
- // prevent default action for disabled buttons
- $('.btn').click(function clickDisabledButtonCallback(e) {
- if ($(this).hasClass('disabled')) {
- e.preventDefault();
- e.stopImmediatePropagation();
- return false;
- }
+/**
+ * TODO: Apparently we are collapsing the right sidebar on certain screensizes per default
+ * except on issue board pages. Why can't we do it with CSS?
+ *
+ * Proposal: Expose a global sidebar API, which we could import wherever we are manipulating
+ * the visibility of the sidebar.
+ *
+ * Quick fix: Get rid of jQuery for this implementation
+ */
+const isBoardsPage = /(projects|groups):boards:show/.test(document.body.dataset.page);
+if (!isBoardsPage && (bootstrapBreakpoint === 'sm' || bootstrapBreakpoint === 'xs')) {
+ const $rightSidebar = $('aside.right-sidebar');
+ const $layoutPage = $('.layout-page');
+
+ if ($rightSidebar.length > 0) {
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $layoutPage.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ $layoutPage.removeClass('right-sidebar-expanded right-sidebar-collapsed');
+ }
+}
- return true;
- });
+// prevent default action for disabled buttons
+$('.btn').click(function clickDisabledButtonCallback(e) {
+ if ($(this).hasClass('disabled')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return false;
+ }
- localTimeAgo(document.querySelectorAll('abbr.timeago, .js-timeago'), true);
-
- /**
- * This disables form buttons while a form is submitting
- * We do not difinitively know all of the places where this is used
- *
- * TODO: Defer execution, migrate to behaviors, and add sentry logging
- */
- $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) {
- const $buttons = $('[type="submit"], .js-disable-on-submit', this).not('.js-no-auto-disable');
- switch (e.type) {
- case 'ajax:beforeSend':
- case 'submit':
- return $buttons.disable();
- default:
- return $buttons.enable();
- }
- });
+ return true;
+});
- $('.navbar-toggler').on('click', () => {
- // The order is important. The `menu-expanded` is used as a source of truth for now.
- // This can be simplified when the :combined_menu feature flag is removed.
- // https://gitlab.com/gitlab-org/gitlab/-/issues/333180
- $('.header-content').toggleClass('menu-expanded');
- navEventHub.$emit(EVENT_RESPONSIVE_TOGGLE);
- });
+localTimeAgo(document.querySelectorAll('abbr.timeago, .js-timeago'), true);
+
+/**
+ * This disables form buttons while a form is submitting
+ * We do not difinitively know all of the places where this is used
+ *
+ * TODO: Defer execution, migrate to behaviors, and add sentry logging
+ */
+$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) {
+ const $buttons = $('[type="submit"], .js-disable-on-submit', this).not('.js-no-auto-disable');
+ switch (e.type) {
+ case 'ajax:beforeSend':
+ case 'submit':
+ return $buttons.disable();
+ default:
+ return $buttons.enable();
+ }
+});
- /**
- * Show suppressed commit diff
- *
- * TODO: Move to commit diff pages
- */
- $document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() {
- const $container = $(this).parent();
- $container.next('table').show();
- $container.remove();
- });
+$('.navbar-toggler').on('click', () => {
+ document.body.classList.toggle('top-nav-responsive-open');
+});
- // Show/hide comments on diff
- $body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) {
- const $this = $(this);
- const notesHolders = $this.closest('.diff-file').find('.notes_holder');
+/**
+ * Show suppressed commit diff
+ *
+ * TODO: Move to commit diff pages
+ */
+$document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() {
+ const $container = $(this).parent();
+ $container.next('table').show();
+ $container.remove();
+});
- e.preventDefault();
+// Show/hide comments on diff
+$body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) {
+ const $this = $(this);
+ const notesHolders = $this.closest('.diff-file').find('.notes_holder');
- $this.toggleClass('selected');
+ e.preventDefault();
- if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
- } else {
- notesHolders.hide().find('.content').hide();
- }
+ $this.toggleClass('selected');
- $(document).trigger('toggle.comments');
- });
+ if ($this.hasClass('active')) {
+ notesHolders.show().find('.hide, .content').show();
+ } else {
+ notesHolders.hide().find('.content').hide();
+ }
- $('form.filter-form').on('submit', function filterFormSubmitCallback(event) {
- const link = document.createElement('a');
- link.href = this.action;
+ $(document).trigger('toggle.comments');
+});
- const action = `${this.action}${link.search === '' ? '?' : '&'}`;
+$('form.filter-form').on('submit', function filterFormSubmitCallback(event) {
+ const link = document.createElement('a');
+ link.href = this.action;
- event.preventDefault();
- // eslint-disable-next-line no-jquery/no-serialize
- visitUrl(`${action}${$(this).serialize()}`);
- });
+ const action = `${this.action}${link.search === '' ? '?' : '&'}`;
- const flashContainer = document.querySelector('.flash-container');
+ event.preventDefault();
+ // eslint-disable-next-line no-jquery/no-serialize
+ visitUrl(`${action}${$(this).serialize()}`);
+});
- if (flashContainer && flashContainer.children.length) {
- flashContainer
- .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
- .forEach((flashEl) => {
- removeFlashClickListener(flashEl);
- });
- }
+const flashContainer = document.querySelector('.flash-container');
- // initialize field errors
- $('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form));
+if (flashContainer && flashContainer.children.length) {
+ flashContainer
+ .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
+ .forEach((flashEl) => {
+ removeFlashClickListener(flashEl);
+ });
+}
- requestIdleCallback(deferredInitialisation);
-});
+// initialize field errors
+$('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form));
+
+requestIdleCallback(deferredInitialisation);
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index a477aedd233..665e8ee69f7 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
export default {
name: 'RemoveMemberButton',
@@ -45,7 +45,7 @@ export default {
oncallSchedules: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
},
computed: {
@@ -54,30 +54,35 @@ export default {
return state[this.namespace].memberPath;
},
}),
- computedMemberPath() {
- return this.memberPath.replace(':id', this.memberId);
- },
- stringifiedSchedules() {
- return JSON.stringify(this.oncallSchedules);
+ modalData() {
+ return {
+ isAccessRequest: this.isAccessRequest,
+ isInvite: this.isInvite,
+ memberPath: this.memberPath.replace(':id', this.memberId),
+ memberType: this.memberType,
+ message: this.message,
+ oncallSchedules: this.oncallSchedules,
+ };
},
},
+ methods: {
+ ...mapActions({
+ showRemoveMemberModal(dispatch, payload) {
+ return dispatch(`${this.namespace}/showRemoveMemberModal`, payload);
+ },
+ }),
+ },
};
</script>
<template>
<gl-button
- v-gl-tooltip.hover
- class="js-remove-member-button"
+ v-gl-tooltip
variant="danger"
:title="title"
:aria-label="title"
:icon="icon"
- :data-member-path="computedMemberPath"
- :data-member-type="memberType"
- :data-is-access-request="isAccessRequest"
- :data-is-invite="isInvite"
- :data-message="message"
- :data-oncall-schedules="stringifiedSchedules"
data-qa-selector="delete_member_button"
+ @click="showRemoveMemberModal(modalData)"
/>
</template>
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 33d86dec767..e9329fb1d88 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,7 +1,12 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import {
+ getParameterByName,
+ setUrlParams,
+ queryToObject,
+ redirectTo,
+} from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
SEARCH_TOKEN_TYPE,
@@ -122,14 +127,16 @@ export default {
const sortParamValue = getParameterByName(SORT_QUERY_PARAM_NAME);
const activeTabParamValue = getParameterByName(ACTIVE_TAB_QUERY_PARAM_NAME);
- window.location.href = setUrlParams(
- {
- ...params,
- ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
- ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
- },
- window.location.href,
- true,
+ redirectTo(
+ setUrlParams(
+ {
+ ...params,
+ ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
+ ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
+ },
+ window.location.href,
+ true,
+ ),
);
},
},
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 7c21e33d892..ee4743010cf 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -1,8 +1,7 @@
<script>
-import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+import { GlTabs, GlTab, GlBadge, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
-// eslint-disable-next-line import/no-deprecated
-import { urlParamsToObject } from '~/lib/utils/url_utility';
+import { queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES, ACTIVE_TAB_QUERY_PARAM_NAME } from '../constants';
import MembersApp from './app.vue';
@@ -36,8 +35,8 @@ export default {
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
},
],
- components: { MembersApp, GlTabs, GlTab, GlBadge },
- inject: ['canManageMembers'],
+ components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
+ inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
data() {
return {
selectedTabIndex: 0,
@@ -59,8 +58,7 @@ export default {
},
}),
urlParams() {
- // eslint-disable-next-line import/no-deprecated
- return Object.keys(urlParamsToObject(window.location.search));
+ return Object.keys(queryToObject(window.location.search, { gatherArrays: true }));
},
activeTabIndexCalculatedFromUrlParams() {
return this.$options.TABS.findIndex(({ namespace }) => {
@@ -123,5 +121,15 @@ export default {
<members-app :namespace="tab.namespace" :tab-query-param-value="tab.queryParamValue" />
</gl-tab>
</template>
+ <template #tabs-end>
+ <gl-button
+ v-if="canExportMembers"
+ class="gl-align-self-center gl-ml-auto"
+ icon="export"
+ :href="exportCsvPath"
+ >
+ {{ __('Export as CSV') }}
+ </gl-button>
+ </template>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index 07272a5b8d6..00b6ebf9a73 100644
--- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -1,7 +1,6 @@
<script>
import { GlFormCheckbox, GlModal } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { mapActions, mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { s__, __ } from '~/locale';
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
@@ -16,20 +15,33 @@ export default {
GlModal,
OncallSchedulesList,
},
- data() {
- return {
- modalData: {},
- };
- },
+ inject: ['namespace'],
computed: {
- isAccessRequest() {
- return parseBoolean(this.modalData.isAccessRequest);
- },
- isInvite() {
- return parseBoolean(this.modalData.isInvite);
- },
+ ...mapState({
+ isAccessRequest(state) {
+ return state[this.namespace].removeMemberModalData.isAccessRequest;
+ },
+ isInvite(state) {
+ return state[this.namespace].removeMemberModalData.isInvite;
+ },
+ memberPath(state) {
+ return state[this.namespace].removeMemberModalData.memberPath;
+ },
+ memberType(state) {
+ return state[this.namespace].removeMemberModalData.memberType;
+ },
+ message(state) {
+ return state[this.namespace].removeMemberModalData.message;
+ },
+ oncallSchedules(state) {
+ return state[this.namespace].removeMemberModalData.oncallSchedules ?? {};
+ },
+ removeMemberModalVisible(state) {
+ return state[this.namespace].removeMemberModalVisible;
+ },
+ }),
isGroupMember() {
- return this.modalData.memberType === 'GroupMember';
+ return this.memberType === 'GroupMember';
},
actionText() {
if (this.isAccessRequest) {
@@ -54,29 +66,13 @@ export default {
isPartOfOncallSchedules() {
return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
},
- oncallSchedules() {
- try {
- return JSON.parse(this.modalData.oncallSchedules);
- } catch (e) {
- Sentry.captureException(e);
- }
- return {};
- },
- },
- mounted() {
- document.addEventListener('click', this.handleClick);
- },
- beforeDestroy() {
- document.removeEventListener('click', this.handleClick);
},
methods: {
- handleClick(event) {
- const removeButton = event.target.closest('.js-remove-member-button');
- if (removeButton) {
- this.modalData = removeButton.dataset;
- this.$refs.modal.show();
- }
- },
+ ...mapActions({
+ hideRemoveMemberModal(dispatch) {
+ return dispatch(`${this.namespace}/hideRemoveMemberModal`);
+ },
+ }),
submitForm() {
this.$refs.form.submit();
},
@@ -91,11 +87,13 @@ export default {
:action-cancel="$options.actionCancel"
:action-primary="actionPrimary"
:title="actionText"
+ :visible="removeMemberModalVisible"
data-qa-selector="remove_member_modal_content"
@primary="submitForm"
+ @hide="hideRemoveMemberModal"
>
- <form ref="form" :action="modalData.memberPath" method="post">
- <p data-testid="modal-message">{{ modalData.message }}</p>
+ <form ref="form" :action="memberPath" method="post">
+ <p>{{ message }}</p>
<oncall-schedules-list
v-if="isPartOfOncallSchedules"
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index b9c80edbc49..debc3fc31f6 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -7,6 +7,7 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
+import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
import ExpiresAt from './expires_at.vue';
@@ -29,6 +30,7 @@ export default {
MemberActionButtons,
RoleDropdown,
RemoveGroupLinkModal,
+ RemoveMemberModal,
ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
@@ -225,6 +227,7 @@ export default {
align="center"
/>
<remove-group-link-modal />
+ <remove-member-modal />
<ldap-override-confirmation-modal />
</div>
</template>
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 2ed0958d1dc..510e89240f4 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -14,7 +14,13 @@ export const initMembersApp = (el, options) => {
Vue.use(Vuex);
Vue.use(GlToast);
- const { sourceId, canManageMembers, ...vuexStoreAttributes } = parseDataAttributes(el);
+ const {
+ sourceId,
+ canManageMembers,
+ canExportMembers,
+ exportCsvPath,
+ ...vuexStoreAttributes
+ } = parseDataAttributes(el);
const modules = Object.keys(MEMBER_TYPES).reduce((accumulator, namespace) => {
const namespacedOptions = options[namespace];
@@ -54,6 +60,8 @@ export const initMembersApp = (el, options) => {
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,
+ canExportMembers,
+ exportCsvPath,
},
render: (createElement) => createElement('members-tabs'),
});
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 7b191dd85d0..712f0d6caa7 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -25,6 +25,14 @@ export const hideRemoveGroupLinkModal = ({ commit }) => {
commit(types.HIDE_REMOVE_GROUP_LINK_MODAL);
};
+export const showRemoveMemberModal = ({ commit }, modalData) => {
+ commit(types.SHOW_REMOVE_MEMBER_MODAL, modalData);
+};
+
+export const hideRemoveMemberModal = ({ commit }) => {
+ commit(types.HIDE_REMOVE_MEMBER_MODAL);
+};
+
export const updateMemberExpiration = async ({ state, commit }, { memberId, expiresAt }) => {
try {
await axios.put(
diff --git a/app/assets/javascripts/members/store/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 77307aa745b..5fa75725552 100644
--- a/app/assets/javascripts/members/store/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
@@ -8,3 +8,6 @@ export const HIDE_ERROR = 'HIDE_ERROR';
export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL';
export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL';
+
+export const SHOW_REMOVE_MEMBER_MODAL = 'SHOW_REMOVE_MEMBER_MODAL';
+export const HIDE_REMOVE_MEMBER_MODAL = 'HIDE_REMOVE_MEMBER_MODAL';
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index f4aac1571d6..b4cf9f3480f 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -47,4 +47,11 @@ export default {
[types.HIDE_REMOVE_GROUP_LINK_MODAL](state) {
state.removeGroupLinkModalVisible = false;
},
+ [types.SHOW_REMOVE_MEMBER_MODAL](state, modalData) {
+ state.removeMemberModalData = modalData;
+ state.removeMemberModalVisible = true;
+ },
+ [types.HIDE_REMOVE_MEMBER_MODAL](state) {
+ state.removeMemberModalVisible = false;
+ },
};
diff --git a/app/assets/javascripts/members/store/state.js b/app/assets/javascripts/members/store/state.js
index 5415b1c5f25..c233a660840 100644
--- a/app/assets/javascripts/members/store/state.js
+++ b/app/assets/javascripts/members/store/state.js
@@ -20,4 +20,6 @@ export default ({
errorMessage: '',
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
+ removeMemberModalData: {},
+ removeMemberModalVisible: false,
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 1d1c0a23fab..14e5e96d7b0 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -82,9 +82,9 @@ export default class MergeRequestTabs {
this.mergeRequestTabPanes && this.mergeRequestTabPanes.querySelectorAll
? this.mergeRequestTabPanes.querySelectorAll('.tab-pane')
: null;
- const navbar = document.querySelector('.navbar-gitlab');
- const peek = document.getElementById('js-peek');
- const paddingTop = 16;
+ this.navbar = document.querySelector('.navbar-gitlab');
+ this.peek = document.getElementById('js-peek');
+ this.paddingTop = 16;
this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
@@ -99,15 +99,6 @@ export default class MergeRequestTabs {
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.clickTab = this.clickTab.bind(this);
- this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
-
- if (peek) {
- this.stickyTop += peek.offsetHeight;
- }
-
- if (this.mergeRequestTabs) {
- this.stickyTop += this.mergeRequestTabs.offsetHeight;
- }
if (stubLocation) {
location = stubLocation;
@@ -520,4 +511,18 @@ export default class MergeRequestTabs {
}
}, 0);
}
+
+ get stickyTop() {
+ let stickyTop = this.navbar ? this.navbar.offsetHeight : 0;
+
+ if (this.peek) {
+ stickyTop += this.peek.offsetHeight;
+ }
+
+ if (this.mergeRequestTabs) {
+ stickyTop += this.mergeRequestTabs.offsetHeight;
+ }
+
+ return stickyTop;
+ }
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index b786d015f3b..446c6b52602 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -404,21 +404,16 @@ export default {
-->
<gl-dropdown
v-gl-tooltip
+ icon="ellipsis_v"
+ :text="__('More actions')"
+ :text-sr-only="true"
toggle-class="gl-px-3!"
no-caret
data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
- <template #button-content>
- <gl-icon class="gl-mr-0!" name="ellipsis_v" />
- </template>
- <gl-dropdown-item
- v-if="expandBtnAvailable"
- ref="expandBtn"
- :href="clipboardText"
- @click.prevent="onExpand"
- >
+ <gl-dropdown-item v-if="expandBtnAvailable" ref="expandBtn" @click.prevent="onExpand">
{{ s__('Metrics|Expand panel') }}
</gl-dropdown-item>
<gl-dropdown-item
diff --git a/app/assets/javascripts/nav/components/responsive_app.vue b/app/assets/javascripts/nav/components/responsive_app.vue
index d601586a3f8..68a39f862fc 100644
--- a/app/assets/javascripts/nav/components/responsive_app.vue
+++ b/app/assets/javascripts/nav/components/responsive_app.vue
@@ -2,8 +2,7 @@
import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
import { BV_DROPDOWN_SHOW, BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '../event_hub';
-import { resetMenuItemsActive, hasMenuExpanded } from '../utils';
+import { resetMenuItemsActive } from '../utils';
import ResponsiveHeader from './responsive_header.vue';
import ResponsiveHome from './responsive_home.vue';
import TopNavContainerView from './top_nav_container_view.vue';
@@ -33,25 +32,14 @@ export default {
},
},
created() {
- eventHub.$on(EVENT_RESPONSIVE_TOGGLE, this.updateResponsiveOpen);
this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay);
this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
-
- this.updateResponsiveOpen();
},
beforeDestroy() {
- eventHub.$off(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay);
this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
},
methods: {
- updateResponsiveOpen() {
- if (hasMenuExpanded()) {
- document.body.classList.add('top-nav-responsive-open');
- } else {
- document.body.classList.remove('top-nav-responsive-open');
- }
- },
onMenuItemClick({ view }) {
if (view) {
this.activeView = view;
diff --git a/app/assets/javascripts/nav/event_hub.js b/app/assets/javascripts/nav/event_hub.js
deleted file mode 100644
index 2c8b1371fe3..00000000000
--- a/app/assets/javascripts/nav/event_hub.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import eventHubFactory from '~/helpers/event_hub_factory';
-
-export const EVENT_RESPONSIVE_TOGGLE = 'top-nav-responsive-toggle';
-
-export default eventHubFactory();
diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js
index 86d6b42e4ea..abd537d2c9a 100644
--- a/app/assets/javascripts/nav/index.js
+++ b/app/assets/javascripts/nav/index.js
@@ -1,4 +1,7 @@
-// With combined_menu feature flag, there's a benefit to splitting up the import
+// TODO: With the combined_menu feature flag removed, there's likely a better
+// way to slice up the async import (i.e., include trigger in main bundle, but
+// async import subviews. Don't do this at the cost of UX).
+// See https://gitlab.com/gitlab-org/gitlab/-/issues/336042
const importModule = () => import(/* webpackChunkName: 'top_nav' */ './mount');
const tryMountTopNav = async () => {
diff --git a/app/assets/javascripts/nav/utils/has_menu_expanded.js b/app/assets/javascripts/nav/utils/has_menu_expanded.js
deleted file mode 100644
index 5f126bbdf76..00000000000
--- a/app/assets/javascripts/nav/utils/has_menu_expanded.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const hasMenuExpanded = () =>
- Boolean(document.querySelector('.header-content.menu-expanded'));
diff --git a/app/assets/javascripts/nav/utils/index.js b/app/assets/javascripts/nav/utils/index.js
index 4fa3d0910da..6d93818f0d3 100644
--- a/app/assets/javascripts/nav/utils/index.js
+++ b/app/assets/javascripts/nav/utils/index.js
@@ -1,2 +1 @@
-export * from './has_menu_expanded';
export * from './reset_menu_items_active';
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index 47d14783d5d..9638c20e28c 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -14,6 +14,11 @@ export default {
type: Object,
required: true,
},
+ noteIsConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
noteableType: {
type: String,
required: false,
@@ -38,6 +43,9 @@ export default {
emailParticipants() {
return this.noteableData.issue_email_participants?.map(({ email }) => email) || [];
},
+ showEmailParticipantsWarning() {
+ return this.emailParticipants.length && !this.noteIsConfidential;
+ },
},
};
</script>
@@ -61,7 +69,7 @@ export default {
/>
<slot></slot>
<email-participants-warning
- v-if="emailParticipants.length"
+ v-if="showEmailParticipantsWarning"
class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100 gl-rounded-base gl-rounded-top-left-none! gl-rounded-top-right-none!"
:emails="emailParticipants"
/>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 9504ed78778..2ebebd76e1e 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -380,6 +380,7 @@ export default {
<comment-field-layout
:with-alert-container="true"
:noteable-data="getNoteableData"
+ :note-is-confidential="noteIsConfidential"
:noteable-type="noteableType"
>
<markdown-field
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index e060a6affd4..b1aee19d5b2 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -21,7 +21,7 @@ export default {
</script>
<template>
- <gl-button :loading="isResolving" class="ml-sm-2" @click="$emit('onClick')">
+ <gl-button :loading="isResolving" class="gl-xs-w-full ml-sm-2" @click="$emit('onClick')">
{{ buttonTitle }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index 5f429cbf462..9119d319d72 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -29,7 +29,7 @@ export default {
:href="url"
:title="$options.i18n.buttonLabel"
:aria-label="$options.i18n.buttonLabel"
- class="new-issue-for-discussion discussion-create-issue-btn"
+ class="new-issue-for-discussion discussion-create-issue-btn gl-xs-w-full"
icon="issue-new"
/>
</div>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 8c5d81c0cc9..9864e91c009 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -115,11 +115,11 @@ export default {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
- handleFormUpdate(note, parentElement, callback, resolveDiscussion) {
- this.$emit('handleFormUpdate', note, parentElement, callback, resolveDiscussion);
+ handleFormUpdate(noteText, parentElement, callback, resolveDiscussion) {
+ this.$emit('handleFormUpdate', { noteText, parentElement, callback, resolveDiscussion });
},
formCancelHandler(shouldConfirm, isDirty) {
- this.$emit('cancelForm', shouldConfirm, isDirty);
+ this.$emit('cancelForm', { shouldConfirm, isDirty });
},
applySuggestion({ suggestionId, flashContainer, callback = () => {}, message }) {
const { discussion_id: discussionId, id: noteId } = this.note;
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 4ce81219f11..f2336e1b6f5 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -326,7 +326,10 @@ export default {
></div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
- <comment-field-layout :noteable-data="getNoteableData">
+ <comment-field-layout
+ :noteable-data="getNoteableData"
+ :note-is-confidential="discussion.confidential"
+ >
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 5ea431224ce..3c6ed0a8aac 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -263,7 +263,7 @@ export default {
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
- formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
+ formUpdateHandler({ noteText, callback, resolveDiscussion }) {
const position = {
...this.note.position,
};
@@ -329,7 +329,7 @@ export default {
}
});
},
- formCancelHandler(shouldConfirm, isDirty) {
+ formCancelHandler({ shouldConfirm, isDirty }) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return;
@@ -392,6 +392,7 @@ export default {
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
+ lazy
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index e4241669fbc..2ce60976adb 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -5,9 +5,12 @@ import initSortDiscussions from './sort_discussions';
import { store } from './stores';
import initTimelineToggle from './timeline';
-const el = document.getElementById('js-vue-notes');
+export default () => {
+ const el = document.getElementById('js-vue-notes');
+ if (!el) {
+ return;
+ }
-if (el) {
// eslint-disable-next-line no-new
new Vue({
el,
@@ -59,4 +62,4 @@ if (el) {
initDiscussionFilters(store);
initSortDiscussions(store);
initTimelineToggle(store);
-}
+};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
new file mode 100644
index 00000000000..4d6a1d5462b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+} from '~/packages_and_registries/package_registry/constants';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ sourceText: s__('PackageRegistry|Source project located at %{link}'),
+ licenseText: s__('PackageRegistry|License information located at %{link}'),
+ recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
+ appGroup: s__('PackageRegistry|App group: %{group}'),
+ appName: s__('PackageRegistry|App name: %{name}'),
+ },
+ components: {
+ DetailsRow,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showMetadata() {
+ return (
+ [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
+ this.packageEntity.packageType,
+ ) && this.packageEntity.metadata
+ );
+ },
+ showNugetMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
+ },
+ showConanMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
+ },
+ showMavenMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_MAVEN;
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="showMetadata">
+ <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3>
+
+ <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
+ <template v-if="showNugetMetadata">
+ <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
+ <gl-sprintf :message="$options.i18n.sourceText">
+ <template #link>
+ <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
+ packageEntity.metadata.projectUrl
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
+ <gl-sprintf :message="$options.i18n.licenseText">
+ <template #link>
+ <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
+ packageEntity.metadata.licenseUrl
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </template>
+
+ <details-row
+ v-else-if="showConanMetadata"
+ icon="information-o"
+ padding="gl-p-4"
+ data-testid="conan-recipe"
+ >
+ <gl-sprintf :message="$options.i18n.recipeText">
+ <template #recipe>{{ packageEntity.metadata.recipe }}</template>
+ </gl-sprintf>
+ </details-row>
+
+ <template v-else-if="showMavenMetadata">
+ <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
+ <gl-sprintf :message="$options.i18n.appName">
+ <template #name>
+ <strong>{{ packageEntity.metadata.appName }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
+ <gl-sprintf :message="$options.i18n.appGroup">
+ <template #group>
+ <strong>{{ packageEntity.metadata.appGroup }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index e2a2fb1430d..3d3fa62fd43 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -1,8 +1,4 @@
<script>
-/*
- * The commented part of this component needs to be re-enabled in the refactor process,
- * See here for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64939
- */
import {
GlBadge,
GlButton,
@@ -14,22 +10,39 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
+import createFlash from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
-// import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
-// import DependencyRow from '~/packages/details/components/dependency_row.vue';
-// import InstallationCommands from '~/packages/details/components/installation_commands.vue';
-// import PackageFiles from '~/packages/details/components/package_files.vue';
-// import PackageHistory from '~/packages/details/components/package_history.vue';
-// import PackageListRow from '~/packages/shared/components/package_list_row.vue';
-import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
+import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
+import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
+import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
+import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
+import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
+import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import {
- PackageType,
- TrackingActions,
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_COMPOSER,
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ PULL_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
SHOW_DELETE_SUCCESS_ALERT,
-} from '~/packages/shared/constants';
-import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+ FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
+ DELETE_PACKAGE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+} from '~/packages_and_registries/package_registry/constants';
+
+import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql';
+import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
import Tracking from '~/tracking';
export default {
@@ -42,16 +55,13 @@ export default {
GlTab,
GlTabs,
GlSprintf,
- PackageTitle: () => import('~/packages/details/components/package_title.vue'),
- TerraformTitle: () =>
- import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
- PackagesListLoader,
- // PackageListRow,
- // DependencyRow,
- // PackageHistory,
- // AdditionalMetadata,
- // InstallationCommands,
- // PackageFiles,
+ PackageTitle,
+ VersionRow,
+ DependencyRow,
+ PackageHistory,
+ AdditionalMetadata,
+ InstallationCommands,
+ PackageFiles,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,7 +69,7 @@ export default {
},
mixins: [Tracking.mixin()],
inject: [
- 'titleComponent',
+ 'packageId',
'projectName',
'canDelete',
'svgPath',
@@ -68,72 +78,150 @@ export default {
'projectListUrl',
'groupListUrl',
],
- trackingActions: { ...TrackingActions },
+ trackingActions: {
+ DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ PULL_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION,
+ },
data() {
return {
fileToDelete: null,
packageEntity: {},
};
},
+ apollo: {
+ packageEntity: {
+ query: getPackageDetails,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.package;
+ },
+ error(error) {
+ createFlash({
+ message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
+ captureError: true,
+ error,
+ });
+ },
+ },
+ },
computed: {
+ queryVariables() {
+ return {
+ id: convertToGraphQLId('Packages::Package', this.packageId),
+ };
+ },
packageFiles() {
- return this.packageEntity.packageFiles;
+ return this.packageEntity?.packageFiles?.nodes;
},
isLoading() {
- return false;
+ return this.$apollo.queries.packageEntity.loading;
},
isValidPackage() {
- return Boolean(this.packageEntity.name);
+ return this.isLoading || Boolean(this.packageEntity?.name);
},
tracking() {
return {
- category: packageTypeToTrackCategory(this.packageEntity.package_type),
+ category: packageTypeToTrackCategory(this.packageEntity.packageType),
};
},
hasVersions() {
- return this.packageEntity.versions?.length > 0;
+ return this.packageEntity.versions?.nodes?.length > 0;
},
packageDependencies() {
- return this.packageEntity.dependency_links || [];
+ return this.packageEntity.dependencyLinks?.nodes || [];
},
showDependencies() {
- return this.packageEntity.package_type === PackageType.NUGET;
+ return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
},
showFiles() {
- return this.packageEntity?.package_type !== PackageType.COMPOSER;
+ return this.packageEntity?.packageType !== PACKAGE_TYPE_COMPOSER;
},
},
methods: {
formatSize(size) {
return numberToHumanSize(size);
},
- getPackageVersions() {
- if (!this.packageEntity.versions) {
- // this.fetchPackageVersions();
+ async deletePackage() {
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackageMutation,
+ variables: {
+ id: this.packageEntity.id,
+ },
+ });
+
+ if (data?.destroyPackage?.errors[0]) {
+ throw data.destroyPackage.errors[0];
}
},
async confirmPackageDeletion() {
- this.track(TrackingActions.DELETE_PACKAGE);
+ this.track(DELETE_PACKAGE_TRACKING_ACTION);
- await this.deletePackage();
+ try {
+ await this.deletePackage();
- const returnTo =
- !this.groupListUrl || document.referrer.includes(this.projectName)
- ? this.projectListUrl
- : this.groupListUrl; // to avoid security issue url are supplied from backend
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
- const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
- window.location.replace(`${returnTo}?${modalQuery}`);
+ window.location.replace(`${returnTo}?${modalQuery}`);
+ } catch (error) {
+ createFlash({
+ message: DELETE_PACKAGE_ERROR_MESSAGE,
+ type: 'warning',
+ captureError: true,
+ error,
+ });
+ }
+ },
+ async deletePackageFile(id) {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackageFileMutation,
+ variables: {
+ id,
+ },
+ awaitRefetchQueries: true,
+ refetchQueries: [
+ {
+ query: getPackageDetails,
+ variables: this.queryVariables,
+ },
+ ],
+ });
+ if (data?.destroyPackageFile?.errors[0]) {
+ throw data.destroyPackageFile.errors[0];
+ }
+ createFlash({
+ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ type: 'success',
+ });
+ } catch (error) {
+ createFlash({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ type: 'warning',
+ captureError: true,
+ error,
+ });
+ }
},
handleFileDelete(file) {
- this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE);
+ this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION);
this.fileToDelete = { ...file };
this.$refs.deleteFileModal.show();
},
confirmFileDelete() {
- this.track(TrackingActions.DELETE_PACKAGE_FILE);
- // this.deletePackageFile(this.fileToDelete.id);
+ this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
+ this.deletePackageFile(this.fileToDelete.id);
this.fileToDelete = null;
},
},
@@ -174,60 +262,48 @@ export default {
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="svgPath"
/>
-
- <div v-else class="packages-app">
- <component :is="titleComponent">
+ <div v-else-if="!isLoading" class="packages-app">
+ <package-title :package-entity="packageEntity">
<template #delete-button>
<gl-button
v-if="canDelete"
v-gl-modal="'delete-modal'"
- class="js-delete-button"
variant="danger"
category="primary"
data-qa-selector="delete_button"
+ data-testid="delete-package"
>
{{ __('Delete') }}
</gl-button>
</template>
- </component>
+ </package-title>
<gl-tabs>
<gl-tab :title="__('Detail')">
- <div data-qa-selector="package_information_content">
- <!-- <package-history :package-entity="packageEntity" :project-name="projectName" />
+ <div v-if="!isLoading" data-qa-selector="package_information_content">
+ <package-history :package-entity="packageEntity" :project-name="projectName" />
- <installation-commands
- :package-entity="packageEntity"
- :npm-path="npmPath"
- :npm-help-path="npmHelpPath"
- />
+ <installation-commands :package-entity="packageEntity" />
- <additional-metadata :package-entity="packageEntity" /> -->
+ <additional-metadata :package-entity="packageEntity" />
</div>
- <!-- <package-files
+ <package-files
v-if="showFiles"
:package-files="packageFiles"
- :can-delete="canDelete"
@download-file="track($options.trackingActions.PULL_PACKAGE)"
@delete-file="handleFileDelete"
- /> -->
+ />
</gl-tab>
- <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
+ <gl-tab v-if="showDependencies">
<template #title>
<span>{{ __('Dependencies') }}</span>
- <gl-badge size="sm" data-testid="dependencies-badge">{{
- packageDependencies.length
- }}</gl-badge>
+ <gl-badge size="sm">{{ packageDependencies.length }}</gl-badge>
</template>
<template v-if="packageDependencies.length > 0">
- <dependency-row
- v-for="(dep, index) in packageDependencies"
- :key="index"
- :dependency="dep"
- />
+ <dependency-row v-for="dep in packageDependencies" :key="dep.id" :dependency-link="dep" />
</template>
<p v-else class="gl-mt-3" data-testid="no-dependencies-message">
@@ -235,24 +311,9 @@ export default {
</p>
</gl-tab>
- <gl-tab
- :title="__('Other versions')"
- title-item-class="js-versions-tab"
- @click="getPackageVersions"
- >
- <template v-if="isLoading && !hasVersions">
- <packages-list-loader />
- </template>
-
- <template v-else-if="hasVersions">
- <!-- <package-list-row
- v-for="v in packageEntity.versions"
- :key="v.id"
- :package-entity="{ name: packageEntity.name, ...v }"
- :package-link="v.id.toString()"
- :disable-delete="true"
- :show-package-type="false"
- /> -->
+ <gl-tab :title="__('Other versions')" title-item-class="js-versions-tab">
+ <template v-if="hasVersions">
+ <version-row v-for="v in packageEntity.versions.nodes" :key="v.id" :package-entity="v" />
</template>
<p v-else class="gl-mt-3" data-testid="no-versions-message">
@@ -263,8 +324,8 @@ export default {
<gl-modal
ref="deleteModal"
- class="js-delete-modal"
modal-id="delete-modal"
+ data-testid="delete-modal"
:action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@primary="confirmPackageDeletion"
@@ -287,6 +348,7 @@ export default {
modal-id="delete-file-modal"
:action-primary="$options.modal.fileDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
+ data-testid="delete-file-modal"
@primary="confirmFileDelete"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue
new file mode 100644
index 00000000000..cc629ae394c
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue
@@ -0,0 +1,87 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
+ TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'ComposerInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['composerHelpPath', 'composerConfigRepositoryName', 'composerPath', 'groupListUrl'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ composerRegistryInclude() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `composer config repositories.${this.composerConfigRepositoryName} '{"type": "composer", "url": "${this.composerPath}"}'`;
+ },
+ composerPackageInclude() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `composer req ${[this.packageEntity.name]}:${this.packageEntity.version}`;
+ },
+ groupExists() {
+ return this.groupListUrl?.length > 0;
+ },
+ },
+ i18n: {
+ registryInclude: s__('PackageRegistry|Add composer registry'),
+ copyRegistryInclude: s__('PackageRegistry|Copy registry include'),
+ packageInclude: s__('PackageRegistry|Install package version'),
+ copyPackageInclude: s__('PackageRegistry|Copy require package include'),
+ infoLine: s__(
+ 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}',
+ ),
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
+ TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ },
+ installOptions: [{ value: 'composer', label: s__('PackageRegistry|Show Composer commands') }],
+};
+</script>
+
+<template>
+ <div v-if="groupExists" data-testid="root-node">
+ <installation-title package-type="composer" :options="$options.installOptions" />
+
+ <code-instruction
+ :label="$options.i18n.registryInclude"
+ :instruction="composerRegistryInclude"
+ :copy-text="$options.i18n.copyRegistryInclude"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ data-testid="registry-include"
+ />
+
+ <code-instruction
+ :label="$options.i18n.packageInclude"
+ :instruction="composerPackageInclude"
+ :copy-text="$options.i18n.copyPackageInclude"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ data-testid="package-include"
+ />
+ <span data-testid="help-text">
+ <gl-sprintf :message="$options.i18n.infoLine">
+ <template #link="{ content }">
+ <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue
new file mode 100644
index 00000000000..99e27c9d44a
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_CONAN_COMMAND,
+ TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'ConanInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['conanHelpPath', 'conanPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ conanInstallationCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `conan install ${this.packageEntity.name} --remote=gitlab`;
+ },
+ conanSetupCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `conan remote add gitlab ${this.conanPath}`;
+ },
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_CONAN_COMMAND,
+ TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ },
+
+ installOptions: [{ value: 'conan', label: s__('PackageRegistry|Show Conan commands') }],
+};
+</script>
+
+<template>
+ <div>
+ <installation-title package-type="conan" :options="$options.installOptions" />
+
+ <code-instruction
+ :label="s__('PackageRegistry|Conan Command')"
+ :instruction="conanInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy Conan Command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_CONAN_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+
+ <code-instruction
+ :label="s__('PackageRegistry|Add Conan Remote')"
+ :instruction="conanSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy Conan Setup Command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="conanHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue
new file mode 100644
index 00000000000..95236eea0b5
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/dependency_row.vue
@@ -0,0 +1,38 @@
+<script>
+export default {
+ name: 'DependencyRow',
+ props: {
+ dependencyLink: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showVersion() {
+ return Boolean(this.dependencyLink.dependency?.versionPattern);
+ },
+ showTargetFramework() {
+ return Boolean(this.dependencyLink.metadata?.targetFramework);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row">
+ <div class="table-section section-50">
+ <strong class="gl-text-body">{{ dependencyLink.dependency.name }}</strong>
+ <span v-if="showTargetFramework" data-testid="target-framework">
+ ({{ dependencyLink.metadata.targetFramework }})
+ </span>
+ </div>
+
+ <div
+ v-if="showVersion"
+ class="table-section section-50 gl-display-flex gl-md-justify-content-end"
+ data-testid="version-pattern"
+ >
+ <span class="gl-text-body">{{ dependencyLink.dependency.versionPattern }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue
new file mode 100644
index 00000000000..a25839be7e1
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/file_sha.vue
@@ -0,0 +1,41 @@
+<script>
+import { s__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ name: 'FileSha',
+ components: {
+ DetailsRow,
+ ClipboardButton,
+ },
+ props: {
+ sha: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ copyButtonTitle: s__('PackageRegistry|Copy SHA'),
+ },
+};
+</script>
+
+<template>
+ <details-row dashed>
+ <div class="gl-px-4">
+ {{ title }}:
+ {{ sha }}
+ <clipboard-button
+ :text="sha"
+ :title="$options.i18n.copyButtonTitle"
+ category="tertiary"
+ size="small"
+ />
+ </div>
+ </details-row>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue
new file mode 100644
index 00000000000..122d444e859
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue
@@ -0,0 +1,45 @@
+<script>
+import {
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_NPM,
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_PYPI,
+ PACKAGE_TYPE_COMPOSER,
+} from '~/packages_and_registries/package_registry/constants';
+import ComposerInstallation from './composer_installation.vue';
+import ConanInstallation from './conan_installation.vue';
+import MavenInstallation from './maven_installation.vue';
+import NpmInstallation from './npm_installation.vue';
+import NugetInstallation from './nuget_installation.vue';
+import PypiInstallation from './pypi_installation.vue';
+
+export default {
+ name: 'InstallationCommands',
+ components: {
+ [PACKAGE_TYPE_CONAN]: ConanInstallation,
+ [PACKAGE_TYPE_MAVEN]: MavenInstallation,
+ [PACKAGE_TYPE_NPM]: NpmInstallation,
+ [PACKAGE_TYPE_NUGET]: NugetInstallation,
+ [PACKAGE_TYPE_PYPI]: PypiInstallation,
+ [PACKAGE_TYPE_COMPOSER]: ComposerInstallation,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ installationComponent() {
+ return this.$options.components[this.packageEntity.packageType];
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="installationComponent">
+ <component :is="installationComponent" :package-entity="packageEntity" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue
new file mode 100644
index 00000000000..43133bf7825
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_title.vue
@@ -0,0 +1,38 @@
+<script>
+import PersistedDropdownSelection from '~/vue_shared/components/registry/persisted_dropdown_selection.vue';
+
+export default {
+ name: 'InstallationTitle',
+ components: {
+ PersistedDropdownSelection,
+ },
+ props: {
+ packageType: {
+ type: String,
+ required: true,
+ },
+ options: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ storageKey() {
+ return `package_${this.packageType}_installation_instructions`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+ <div>
+ <persisted-dropdown-selection
+ :storage-key="storageKey"
+ :options="options"
+ @change="$emit('change', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue
new file mode 100644
index 00000000000..2070f0bbca0
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue
@@ -0,0 +1,229 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_MAVEN_XML,
+ TRACKING_ACTION_COPY_MAVEN_COMMAND,
+ TRACKING_ACTION_COPY_MAVEN_SETUP,
+ TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND,
+ TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ TRACKING_LABEL_MAVEN_INSTALLATION,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'MavenInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['mavenHelpPath', 'mavenPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ instructionType: 'maven',
+ };
+ },
+ computed: {
+ appGroup() {
+ return this.packageEntity.metadata.appGroup;
+ },
+ appName() {
+ return this.packageEntity.metadata.appName;
+ },
+ appVersion() {
+ return this.packageEntity.metadata.appVersion;
+ },
+ mavenInstallationXml() {
+ return `<dependency>
+ <groupId>${this.appGroup}</groupId>
+ <artifactId>${this.appName}</artifactId>
+ <version>${this.appVersion}</version>
+</dependency>`;
+ },
+
+ mavenInstallationCommand() {
+ return `mvn dependency:get -Dartifact=${this.appGroup}:${this.appName}:${this.appVersion}`;
+ },
+
+ mavenSetupXml() {
+ return `<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${this.mavenPath}</url>
+ </repository>
+</repositories>
+
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${this.mavenPath}</url>
+ </repository>
+
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>${this.mavenPath}</url>
+ </snapshotRepository>
+</distributionManagement>`;
+ },
+
+ gradleGroovyInstalCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `implementation '${this.appGroup}:${this.appName}:${this.appVersion}'`;
+ },
+
+ gradleGroovyAddSourceCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `maven {
+ url '${this.mavenPath}'
+}`;
+ },
+
+ gradleKotlinInstalCommand() {
+ return `implementation("${this.appGroup}:${this.appName}:${this.appVersion}")`;
+ },
+
+ gradleKotlinAddSourceCommand() {
+ return `maven("${this.mavenPath}")`;
+ },
+ showMaven() {
+ return this.instructionType === 'maven';
+ },
+ showGroovy() {
+ return this.instructionType === 'groovy';
+ },
+ },
+ i18n: {
+ xmlText: s__(
+ `PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block.`,
+ ),
+ setupText: s__(
+ `PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file.`,
+ ),
+ helpText: s__(
+ 'PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_MAVEN_XML,
+ TRACKING_ACTION_COPY_MAVEN_COMMAND,
+ TRACKING_ACTION_COPY_MAVEN_SETUP,
+ TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND,
+ TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ TRACKING_LABEL_MAVEN_INSTALLATION,
+ },
+
+ installOptions: [
+ { value: 'maven', label: s__('PackageRegistry|Maven XML') },
+ { value: 'groovy', label: s__('PackageRegistry|Gradle Groovy DSL') },
+ { value: 'kotlin', label: s__('PackageRegistry|Gradle Kotlin DSL') },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <installation-title
+ package-type="maven"
+ :options="$options.installOptions"
+ @change="instructionType = $event"
+ />
+
+ <template v-if="showMaven">
+ <p>
+ <gl-sprintf :message="$options.i18n.xmlText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <code-instruction
+ :instruction="mavenInstallationXml"
+ :copy-text="s__('PackageRegistry|Copy Maven XML')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_XML"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ multiline
+ />
+
+ <code-instruction
+ :label="s__('PackageRegistry|Maven Command')"
+ :instruction="mavenInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy Maven command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <h3 class="gl-font-lg">{{ s__('PackageRegistry|Registry setup') }}</h3>
+ <p>
+ <gl-sprintf :message="$options.i18n.setupText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <code-instruction
+ :instruction="mavenSetupXml"
+ :copy-text="s__('PackageRegistry|Copy Maven registry XML')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_SETUP"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ multiline
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <template v-else-if="showGroovy">
+ <code-instruction
+ class="gl-mb-5"
+ :label="s__('PackageRegistry|Gradle Groovy DSL install command')"
+ :instruction="gradleGroovyInstalCommand"
+ :copy-text="s__('PackageRegistry|Copy Gradle Groovy DSL install command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+ <code-instruction
+ :label="s__('PackageRegistry|Add Gradle Groovy DSL repository command')"
+ :instruction="gradleGroovyAddSourceCommand"
+ :copy-text="s__('PackageRegistry|Copy add Gradle Groovy DSL repository command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ 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.tracking.TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_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.tracking.TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ multiline
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
new file mode 100644
index 00000000000..47081e23318
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
@@ -0,0 +1,141 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ NPM_PACKAGE_MANAGER,
+ YARN_PACKAGE_MANAGER,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'NpmInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['npmHelpPath', 'npmPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ instructionType: NPM_PACKAGE_MANAGER,
+ };
+ },
+ computed: {
+ npmCommand() {
+ return this.npmInstallationCommand(NPM_PACKAGE_MANAGER);
+ },
+ npmSetup() {
+ return this.npmSetupCommand(NPM_PACKAGE_MANAGER);
+ },
+ yarnCommand() {
+ return this.npmInstallationCommand(YARN_PACKAGE_MANAGER);
+ },
+ yarnSetupCommand() {
+ return this.npmSetupCommand(YARN_PACKAGE_MANAGER);
+ },
+ showNpm() {
+ return this.instructionType === NPM_PACKAGE_MANAGER;
+ },
+ },
+ methods: {
+ npmInstallationCommand(type) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const instruction = type === NPM_PACKAGE_MANAGER ? 'npm i' : 'yarn add';
+
+ return `${instruction} ${this.packageEntity.name}`;
+ },
+ npmSetupCommand(type) {
+ const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/'));
+
+ if (type === NPM_PACKAGE_MANAGER) {
+ return `echo ${scope}:registry=${this.npmPath}/ >> .npmrc`;
+ }
+
+ return `echo \\"${scope}:registry\\" \\"${this.npmPath}/\\" >> .yarnrc`;
+ },
+ },
+ packageManagers: {
+ NPM_PACKAGE_MANAGER,
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NPM_SETUP_COMMAND,
+ TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_YARN_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.',
+ ),
+ },
+ installOptions: [
+ { value: NPM_PACKAGE_MANAGER, label: s__('PackageRegistry|Show NPM commands') },
+ { value: YARN_PACKAGE_MANAGER, label: s__('PackageRegistry|Show Yarn commands') },
+ ],
+};
+</script>
+
+<template>
+ <div>
+ <installation-title
+ :package-type="$options.packageManagers.NPM_PACKAGE_MANAGER"
+ :options="$options.installOptions"
+ @change="instructionType = $event"
+ />
+
+ <code-instruction
+ v-if="showNpm"
+ :instruction="npmCommand"
+ :copy-text="s__('PackageRegistry|Copy npm command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <code-instruction
+ v-else
+ :instruction="yarnCommand"
+ :copy-text="s__('PackageRegistry|Copy yarn command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+
+ <code-instruction
+ v-if="showNpm"
+ :instruction="npmSetup"
+ :copy-text="s__('PackageRegistry|Copy npm setup command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NPM_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <code-instruction
+ v-else
+ :instruction="yarnSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy yarn setup command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_YARN_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="npmHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
new file mode 100644
index 00000000000..2e9991b7be5
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'NugetInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['nugetHelpPath', 'nugetPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ nugetInstallationCommand() {
+ return `nuget install ${this.packageEntity.name} -Source "GitLab"`;
+ },
+ nugetSetupCommand() {
+ return `nuget source Add -Name "GitLab" -Source "${this.nugetPath}" -UserName <your_username> -Password <your_token>`;
+ },
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ installOptions: [{ value: 'nuget', label: s__('PackageRegistry|Show Nuget commands') }],
+};
+</script>
+
+<template>
+ <div>
+ <installation-title package-type="nuget" :options="$options.installOptions" />
+
+ <code-instruction
+ :label="s__('PackageRegistry|NuGet Command')"
+ :instruction="nugetInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy NuGet Command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+
+ <code-instruction
+ :label="s__('PackageRegistry|Add NuGet Source')"
+ :instruction="nugetSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="nugetHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
new file mode 100644
index 00000000000..bf7fe6fb91b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -0,0 +1,163 @@
+<script>
+import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui';
+import { last } from 'lodash';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue';
+import Tracking from '~/tracking';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ name: 'PackageFiles',
+ components: {
+ GlLink,
+ GlTable,
+ GlIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlButton,
+ FileIcon,
+ TimeAgoTooltip,
+ FileSha,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['canDelete'],
+ props: {
+ packageFiles: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ filesTableRows() {
+ return this.packageFiles.map((pf) => ({
+ ...pf,
+ size: this.formatSize(pf.size),
+ pipeline: last(pf.pipelines),
+ }));
+ },
+ showCommitColumn() {
+ // note that this is always false for now since we do not return
+ // pipelines associated to files for performance concerns
+ return this.filesTableRows.some((row) => Boolean(row.pipeline?.id));
+ },
+ filesTableHeaderFields() {
+ return [
+ {
+ key: 'name',
+ label: __('Name'),
+ },
+ {
+ key: 'commit',
+ label: __('Commit'),
+ hide: !this.showCommitColumn,
+ },
+ {
+ key: 'size',
+ label: __('Size'),
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ class: 'gl-text-right',
+ },
+ {
+ key: 'actions',
+ label: '',
+ hide: !this.canDelete,
+ class: 'gl-text-right',
+ tdClass: 'gl-w-4',
+ },
+ ].filter((c) => !c.hide);
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ hasDetails(item) {
+ return item.fileSha256 || item.fileMd5 || item.fileSha1;
+ },
+ },
+ i18n: {
+ deleteFile: __('Delete file'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
+ <gl-table
+ :fields="filesTableHeaderFields"
+ :items="filesTableRows"
+ :tbody-tr-attr="{ 'data-testid': 'file-row' }"
+ >
+ <template #cell(name)="{ item, toggleDetails, detailsShowing }">
+ <gl-button
+ v-if="hasDetails(item)"
+ :icon="detailsShowing ? 'angle-up' : 'angle-down'"
+ :aria-label="detailsShowing ? __('Collapse') : __('Expand')"
+ category="tertiary"
+ size="small"
+ @click="toggleDetails"
+ />
+ <gl-link
+ :href="item.downloadPath"
+ class="gl-text-gray-500"
+ data-testid="download-link"
+ @click="$emit('download-file')"
+ >
+ <file-icon
+ :file-name="item.fileName"
+ css-classes="gl-relative file-icon"
+ class="gl-mr-1 gl-relative"
+ />
+ <span>{{ item.fileName }}</span>
+ </gl-link>
+ </template>
+
+ <template #cell(commit)="{ item }">
+ <gl-link
+ v-if="item.pipeline && item.pipeline"
+ :href="item.pipeline.commitPath"
+ class="gl-text-gray-500"
+ data-testid="commit-link"
+ >{{ item.pipeline.sha }}</gl-link
+ >
+ </template>
+
+ <template #cell(created)="{ item }">
+ <time-ago-tooltip :time="item.createdAt" />
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <gl-dropdown category="tertiary" right>
+ <template #button-content>
+ <gl-icon name="ellipsis_v" />
+ </template>
+ <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-file', item)">
+ {{ $options.i18n.deleteFile }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </template>
+
+ <template #row-details="{ item }">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100"
+ >
+ <file-sha
+ v-if="item.fileSha256"
+ data-testid="sha-256"
+ title="SHA-256"
+ :sha="item.fileSha256"
+ />
+ <file-sha v-if="item.fileMd5" data-testid="md5" title="MD5" :sha="item.fileMd5" />
+ <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" />
+ </div>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
new file mode 100644
index 00000000000..af4a984add4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
@@ -0,0 +1,169 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { first } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { truncateSha } from '~/lib/utils/text_utility';
+import { s__, n__ } from '~/locale';
+import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ name: 'PackageHistory',
+ i18n: {
+ createdOn: s__('PackageRegistry|%{name} version %{version} was first created %{datetime}'),
+ createdByCommitText: s__('PackageRegistry|Created by commit %{link} on branch %{branch}'),
+ createdByPipelineText: s__(
+ 'PackageRegistry|Built by pipeline %{link} triggered %{datetime} by %{author}',
+ ),
+ publishText: s__('PackageRegistry|Published to the %{project} Package Registry %{datetime}'),
+ combinedUpdateText: s__(
+ 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}',
+ ),
+ archivedPipelineMessageSingular: s__('PackageRegistry|Package has %{number} archived update'),
+ archivedPipelineMessagePlural: s__('PackageRegistry|Package has %{number} archived updates'),
+ },
+ components: {
+ GlLink,
+ GlSprintf,
+ HistoryItem,
+ TimeAgoTooltip,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showDescription: false,
+ };
+ },
+ computed: {
+ pipelines() {
+ return this.packageEntity?.pipelines?.nodes || [];
+ },
+ firstPipeline() {
+ return first(this.pipelines);
+ },
+ lastPipelines() {
+ return this.pipelines.slice(1).slice(-HISTORY_PIPELINES_LIMIT);
+ },
+ showPipelinesInfo() {
+ return Boolean(this.firstPipeline?.id);
+ },
+ archiviedLines() {
+ return Math.max(this.pipelines.length - HISTORY_PIPELINES_LIMIT - 1, 0);
+ },
+ archivedPipelineMessage() {
+ return n__(
+ this.$options.i18n.archivedPipelineMessageSingular,
+ this.$options.i18n.archivedPipelineMessagePlural,
+ this.archiviedLines,
+ );
+ },
+ },
+ methods: {
+ truncate(value) {
+ return truncateSha(value);
+ },
+ convertToBaseId(value) {
+ return getIdFromGraphQLId(value);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-discussion">
+ <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
+ <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
+ <history-item icon="clock" data-testid="created-on">
+ <gl-sprintf :message="$options.i18n.createdOn">
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packageEntity.createdAt" />
+ </template>
+ </gl-sprintf>
+ </history-item>
+
+ <template v-if="showPipelinesInfo">
+ <!-- FIRST PIPELINE BLOCK -->
+ <history-item icon="commit" data-testid="first-pipeline-commit">
+ <gl-sprintf :message="$options.i18n.createdByCommitText">
+ <template #link>
+ <gl-link :href="firstPipeline.commitPath">#{{ truncate(firstPipeline.sha) }}</gl-link>
+ </template>
+ <template #branch>
+ <strong>{{ firstPipeline.ref }}</strong>
+ </template>
+ </gl-sprintf>
+ </history-item>
+ <history-item icon="pipeline" data-testid="first-pipeline-pipeline">
+ <gl-sprintf :message="$options.i18n.createdByPipelineText">
+ <template #link>
+ <gl-link :href="firstPipeline.path">#{{ convertToBaseId(firstPipeline.id) }}</gl-link>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="firstPipeline.createdAt" />
+ </template>
+ <template #author>{{ firstPipeline.user.name }}</template>
+ </gl-sprintf>
+ </history-item>
+ </template>
+
+ <!-- PUBLISHED LINE -->
+ <history-item icon="package" data-testid="published">
+ <gl-sprintf :message="$options.i18n.publishText">
+ <template #project>
+ <strong>{{ projectName }}</strong>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packageEntity.createdAt" />
+ </template>
+ </gl-sprintf>
+ </history-item>
+
+ <history-item v-if="archiviedLines" icon="history" data-testid="archived">
+ <gl-sprintf :message="archivedPipelineMessage">
+ <template #number>
+ <strong>{{ archiviedLines }}</strong>
+ </template>
+ </gl-sprintf>
+ </history-item>
+
+ <!-- PIPELINES LIST ENTRIES -->
+ <history-item
+ v-for="pipeline in lastPipelines"
+ :key="pipeline.id"
+ icon="pencil"
+ data-testid="pipeline-entry"
+ >
+ <gl-sprintf :message="$options.i18n.combinedUpdateText">
+ <template #link>
+ <gl-link :href="pipeline.commitPath">#{{ truncate(pipeline.sha) }}</gl-link>
+ </template>
+ <template #branch>
+ <strong>{{ pipeline.ref }}</strong>
+ </template>
+ <template #pipeline>
+ <gl-link :href="pipeline.path">#{{ convertToBaseId(pipeline.id) }}</gl-link>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="pipeline.createdAt" />
+ </template>
+ </gl-sprintf>
+ </history-item>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
new file mode 100644
index 00000000000..65547af3913
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlIcon, GlSprintf, GlBadge } from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
+import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ name: 'PackageTitle',
+ components: {
+ TitleArea,
+ GlIcon,
+ GlSprintf,
+ PackageTags,
+ MetadataItem,
+ GlBadge,
+ TimeAgoTooltip,
+ },
+ i18n: {
+ packageInfo: __('v%{version} published %{timeAgo}'),
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isDesktop: true,
+ };
+ },
+ computed: {
+ packageTypeDisplay() {
+ return getPackageTypeLabel(this.packageEntity.packageType);
+ },
+ packagePipeline() {
+ return this.packageEntity.pipelines?.nodes[0];
+ },
+ packageIcon() {
+ if (this.packageEntity.packageType === PACKAGE_TYPE_NUGET) {
+ return this.packageEntity.metadata?.iconUrl || null;
+ }
+ return null;
+ },
+ hasTagsToDisplay() {
+ return Boolean(this.packageEntity.tags?.nodes && this.packageEntity.tags?.nodes.length);
+ },
+ totalSize() {
+ return this.packageEntity.packageFiles
+ ? numberToHumanSize(
+ this.packageEntity.packageFiles.nodes.reduce((acc, p) => acc + Number(p.size), 0),
+ )
+ : '0';
+ },
+ },
+ mounted() {
+ this.isDesktop = GlBreakpointInstance.isDesktop();
+ },
+ methods: {
+ dynamicSlotName(index) {
+ return `metadata-tag${index}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
+ <template #sub-header>
+ <gl-icon name="eye" class="gl-mr-3" />
+ <span data-testid="sub-header">
+ <gl-sprintf :message="$options.i18n.packageInfo">
+ <template #version>
+ {{ packageEntity.version }}
+ </template>
+
+ <template #timeAgo>
+ <time-ago-tooltip
+ v-if="packageEntity.createdAt"
+ class="gl-ml-2"
+ :time="packageEntity.createdAt"
+ />
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+
+ <template v-if="packageTypeDisplay" #metadata-type>
+ <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
+ </template>
+
+ <template #metadata-size>
+ <metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
+ </template>
+
+ <template v-if="packagePipeline" #metadata-pipeline>
+ <metadata-item
+ data-testid="pipeline-project"
+ icon="review-list"
+ :text="packagePipeline.project.name"
+ :link="packagePipeline.project.webUrl"
+ />
+ </template>
+
+ <template v-if="packagePipeline && packagePipeline.ref" #metadata-ref>
+ <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
+ </template>
+
+ <template v-if="isDesktop && hasTagsToDisplay" #metadata-tags>
+ <package-tags :tag-display-limit="2" :tags="packageEntity.tags.nodes" hide-label />
+ </template>
+
+ <!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap -->
+ <template
+ v-for="(tag, index) in packageEntity.tags.nodes"
+ v-else-if="hasTagsToDisplay"
+ #[dynamicSlotName(index)]
+ >
+ <gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm">
+ {{ tag.name }}
+ </gl-badge>
+ </template>
+
+ <template #right-actions>
+ <slot name="delete-button"></slot>
+ </template>
+ </title-area>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
new file mode 100644
index 00000000000..669adab9df6
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
@@ -0,0 +1,93 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue';
+import {
+ TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+} from '~/packages_and_registries/package_registry/constants';
+import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
+
+export default {
+ name: 'PyPiInstallation',
+ components: {
+ InstallationTitle,
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['pypiHelpPath', 'pypiPath', 'pypiSetupPath'],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pypiPipCommand() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `pip install ${this.packageEntity.name} --extra-index-url ${this.pypiPath}`;
+ },
+ pypiSetupCommand() {
+ return `[gitlab]
+repository = ${this.pypiSetupPath}
+username = __token__
+password = <your personal access token>`;
+ },
+ },
+ tracking: {
+ TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
+ TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
+ TRACKING_LABEL_CODE_INSTRUCTION,
+ },
+ i18n: {
+ setupText: s__(
+ `PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file.`,
+ ),
+ helpText: s__(
+ 'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ installOptions: [{ value: 'pypi', label: s__('PackageRegistry|Show PyPi commands') }],
+};
+</script>
+
+<template>
+ <div>
+ <installation-title package-type="pypi" :options="$options.installOptions" />
+
+ <code-instruction
+ :label="s__('PackageRegistry|Pip Command')"
+ :instruction="pypiPipCommand"
+ :copy-text="s__('PackageRegistry|Copy Pip command')"
+ data-testid="pip-command"
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+ <p>
+ <gl-sprintf :message="$options.i18n.setupText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <code-instruction
+ :instruction="pypiSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy .pypirc content')"
+ data-testid="pypi-setup-content"
+ multiline
+ :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND"
+ :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
new file mode 100644
index 00000000000..d218a405af6
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import PublishMethod from '~/packages/shared/components/publish_method.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { PACKAGE_DEFAULT_STATUS } from '../../constants';
+
+export default {
+ name: 'PackageListRow',
+ components: {
+ GlLink,
+ GlSprintf,
+ GlTruncate,
+ PackageTags,
+ PublishMethod,
+ ListItem,
+ TimeAgoTooltip,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ packageLink() {
+ return `${getIdFromGraphQLId(this.packageEntity.id)}`;
+ },
+ disabledRow() {
+ return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
+ },
+ },
+};
+</script>
+
+<template>
+ <list-item :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" :disabled="disabledRow">
+ <gl-truncate :text="packageEntity.name" />
+ </gl-link>
+
+ <package-tags
+ v-if="packageEntity.tags.nodes && packageEntity.tags.nodes.length"
+ class="gl-ml-3"
+ :tags="packageEntity.tags.nodes"
+ hide-label
+ :tag-display-limit="1"
+ />
+ </div>
+ </template>
+ <template #left-secondary>
+ {{ packageEntity.version }}
+ </template>
+
+ <template #right-primary>
+ <publish-method :package-entity="packageEntity" />
+ </template>
+
+ <template #right-secondary>
+ <gl-sprintf :message="__('Created %{timestamp}')">
+ <template #timestamp>
+ <time-ago-tooltip :time="packageEntity.createdAt" />
+ </template>
+ </gl-sprintf>
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
new file mode 100644
index 00000000000..aad888b4433
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -0,0 +1,88 @@
+import { __, s__ } from '~/locale';
+
+export const PACKAGE_TYPE_CONAN = 'CONAN';
+export const PACKAGE_TYPE_MAVEN = 'MAVEN';
+export const PACKAGE_TYPE_NPM = 'NPM';
+export const PACKAGE_TYPE_NUGET = 'NUGET';
+export const PACKAGE_TYPE_PYPI = 'PYPI';
+export const PACKAGE_TYPE_COMPOSER = 'COMPOSER';
+export const PACKAGE_TYPE_RUBYGEMS = 'RUBYGEMS';
+export const PACKAGE_TYPE_GENERIC = 'GENERIC';
+export const PACKAGE_TYPE_DEBIAN = 'DEBIAN';
+export const PACKAGE_TYPE_HELM = 'HELM';
+
+export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
+export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package';
+export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package';
+export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package';
+export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file';
+export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file';
+export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file';
+
+export const TRACKING_LABEL_CODE_INSTRUCTION = 'code_instruction';
+export const TRACKING_LABEL_CONAN_INSTALLATION = 'conan_installation';
+export const TRACKING_LABEL_MAVEN_INSTALLATION = 'maven_installation';
+export const TRACKING_LABEL_NPM_INSTALLATION = 'npm_installation';
+export const TRACKING_LABEL_NUGET_INSTALLATION = 'nuget_installation';
+export const TRACKING_LABEL_PYPI_INSTALLATION = 'pypi_installation';
+export const TRACKING_LABEL_COMPOSER_INSTALLATION = 'composer_installation';
+
+export const TRACKING_ACTION_INSTALLATION = 'installation';
+export const TRACKING_ACTION_REGISTRY_SETUP = 'registry_setup';
+
+export const TRACKING_ACTION_COPY_CONAN_COMMAND = 'copy_conan_command';
+export const TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND = 'copy_conan_setup_command';
+
+export const TRACKING_ACTION_COPY_MAVEN_XML = 'copy_maven_xml';
+export const TRACKING_ACTION_COPY_MAVEN_COMMAND = 'copy_maven_command';
+export const TRACKING_ACTION_COPY_MAVEN_SETUP = 'copy_maven_setup_xml';
+export const TRACKING_ACTION_COPY_GRADLE_INSTALL_COMMAND = 'copy_gradle_install_command';
+export const TRACKING_ACTION_COPY_GRADLE_ADD_TO_SOURCE_COMMAND =
+ 'copy_gradle_add_to_source_command';
+export const TRACKING_ACTION_COPY_KOTLIN_INSTALL_COMMAND = 'copy_kotlin_install_command';
+export const TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND =
+ 'copy_kotlin_add_to_source_command';
+
+export const TRACKING_ACTION_COPY_NPM_INSTALL_COMMAND = 'copy_npm_install_command';
+export const TRACKING_ACTION_COPY_NPM_SETUP_COMMAND = 'copy_npm_setup_command';
+export const TRACKING_ACTION_COPY_YARN_INSTALL_COMMAND = 'copy_yarn_install_command';
+export const TRACKING_ACTION_COPY_YARN_SETUP_COMMAND = 'copy_yarn_setup_command';
+
+export const TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND = 'copy_nuget_install_command';
+export const TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND = 'copy_nuget_setup_command';
+
+export const TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND = 'copy_pip_install_command';
+export const TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND = 'copy_pypi_setup_command';
+
+export const TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND =
+ 'copy_composer_registry_include_command';
+export const TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND =
+ 'copy_composer_package_include_command';
+
+export const TrackingCategories = {
+ [PACKAGE_TYPE_MAVEN]: 'MavenPackages',
+ [PACKAGE_TYPE_NPM]: 'NpmPackages',
+ [PACKAGE_TYPE_CONAN]: 'ConanPackages',
+};
+
+export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while deleting the package.',
+);
+export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
+ __('PackageRegistry|Something went wrong while deleting the package file.'),
+);
+export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
+ 'PackageRegistry|Package file deleted successfully',
+);
+export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Failed to load the package data',
+);
+
+export const PACKAGE_ERROR_STATUS = 'ERROR';
+export const PACKAGE_DEFAULT_STATUS = 'DEFAULT';
+export const PACKAGE_HIDDEN_STATUS = 'HIDDEN';
+export const PACKAGE_PROCESSING_STATUS = 'PROCESSING';
+
+export const NPM_PACKAGE_MANAGER = 'npm';
+export const YARN_PACKAGE_MANAGER = 'yarn';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..c61a653d10b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json
@@ -0,0 +1,17 @@
+{
+ "__schema": {
+ "types": [
+ {
+ "kind": "UNION",
+ "name": "PackageMetadata",
+ "possibleTypes": [
+ { "name": "ComposerMetadata" },
+ { "name": "ConanMetadata" },
+ { "name": "MavenMetadata" },
+ { "name": "NugetMetadata" },
+ { "name": "PypiMetadata" }
+ ]
+ }
+ ]
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
new file mode 100644
index 00000000000..f8cb5c516e2
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
@@ -0,0 +1,23 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
+
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+Vue.use(VueApollo);
+
+export const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ assumeImmutableResults: true,
+ },
+ ),
+});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql
new file mode 100644
index 00000000000..884980f24a9
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql
@@ -0,0 +1,5 @@
+mutation destroyPackage($id: PackagesPackageID!) {
+ destroyPackage(input: { id: $id }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql
new file mode 100644
index 00000000000..f016640f57d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql
@@ -0,0 +1,5 @@
+mutation destroyPackageFile($id: PackagesPackageFileID!) {
+ destroyPackageFile(input: { id: $id }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
new file mode 100644
index 00000000000..14aa14e9822
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -0,0 +1,111 @@
+query getPackageDetails($id: ID!) {
+ package(id: $id) {
+ id
+ name
+ packageType
+ version
+ createdAt
+ updatedAt
+ status
+ project {
+ path
+ }
+ tags(first: 10) {
+ nodes {
+ id
+ name
+ }
+ }
+ pipelines(first: 10) {
+ nodes {
+ ref
+ id
+ sha
+ createdAt
+ commitPath
+ path
+ user {
+ name
+ }
+ project {
+ name
+ webUrl
+ }
+ }
+ }
+ packageFiles(first: 100) {
+ nodes {
+ id
+ fileMd5
+ fileName
+ fileSha1
+ fileSha256
+ size
+ createdAt
+ downloadPath
+ }
+ }
+ versions(first: 100) {
+ nodes {
+ id
+ name
+ createdAt
+ version
+ status
+ tags(first: 1) {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ dependencyLinks {
+ nodes {
+ id
+ dependency {
+ id
+ name
+ versionPattern
+ }
+ dependencyType
+ metadata {
+ ... on NugetDependencyLinkMetadata {
+ id
+ targetFramework
+ }
+ }
+ }
+ }
+ metadata {
+ ... on ComposerMetadata {
+ targetSha
+ composerJson {
+ license
+ version
+ }
+ }
+ ... on PypiMetadata {
+ requiredPython
+ }
+ ... on ConanMetadata {
+ packageChannel
+ packageUsername
+ recipe
+ recipePath
+ }
+ ... on MavenMetadata {
+ appName
+ appGroup
+ appVersion
+ path
+ }
+
+ ... on NugetMetadata {
+ iconUrl
+ licenseUrl
+ projectUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
index 309b35a8084..d94bbd21035 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
+import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import Translate from '~/vue_shared/translate';
-import PackagesApp from '../components/details/app.vue';
Vue.use(Translate);
@@ -14,9 +15,9 @@ export default () => {
const { canDelete, ...datasetOptions } = el.dataset;
return new Vue({
el,
+ apolloProvider,
provide: {
canDelete: parseBoolean(canDelete),
- titleComponent: 'PackageTitle',
...datasetOptions,
},
render(createElement) {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/utils.js b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
new file mode 100644
index 00000000000..ae886952c3e
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
@@ -0,0 +1,40 @@
+import { s__ } from '~/locale';
+import {
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_NPM,
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_PYPI,
+ PACKAGE_TYPE_COMPOSER,
+ PACKAGE_TYPE_RUBYGEMS,
+ PACKAGE_TYPE_GENERIC,
+ PACKAGE_TYPE_DEBIAN,
+ PACKAGE_TYPE_HELM,
+} from './constants';
+
+export const getPackageTypeLabel = (packageType) => {
+ switch (packageType) {
+ case PACKAGE_TYPE_CONAN:
+ return s__('PackageRegistry|Conan');
+ case PACKAGE_TYPE_MAVEN:
+ return s__('PackageRegistry|Maven');
+ case PACKAGE_TYPE_NPM:
+ return s__('PackageRegistry|npm');
+ case PACKAGE_TYPE_NUGET:
+ return s__('PackageRegistry|NuGet');
+ case PACKAGE_TYPE_PYPI:
+ return s__('PackageRegistry|PyPI');
+ case PACKAGE_TYPE_RUBYGEMS:
+ return s__('PackageRegistry|RubyGems');
+ case PACKAGE_TYPE_COMPOSER:
+ return s__('PackageRegistry|Composer');
+ case PACKAGE_TYPE_GENERIC:
+ return s__('PackageRegistry|Generic');
+ case PACKAGE_TYPE_DEBIAN:
+ return s__('PackageRegistry|Debian');
+ case PACKAGE_TYPE_HELM:
+ return s__('PackageRegistry|Helm');
+ default:
+ return null;
+ }
+};
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index 9850113d4be..c2510a16d2f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -11,7 +11,7 @@ import {
} from '@gitlab/ui';
import { toSafeInteger } from 'lodash';
import csrf from '~/lib/utils/csrf';
-import { __, s__, sprintf } from '~/locale';
+import { __, n__, s__, sprintf } from '~/locale';
import SignupCheckbox from './signup_checkbox.vue';
const DENYLIST_TYPE_RAW = 'raw';
@@ -51,6 +51,7 @@ export default {
'supportedSyntaxLinkUrl',
'emailRestrictions',
'afterSignUpText',
+ 'pendingUserCount',
],
data() {
return {
@@ -105,8 +106,9 @@ export default {
canUsersBeAccidentallyApproved() {
const hasUserCapBeenToggledOff =
this.requireAdminApprovalAfterUserSignup && !this.form.requireAdminApproval;
+ const currentlyPendingUsers = this.pendingUserCount > 0;
- return this.hasUserCapBeenIncreased || hasUserCapBeenToggledOff;
+ return (this.hasUserCapBeenIncreased || hasUserCapBeenToggledOff) && currentlyPendingUsers;
},
signupEnabledHelpText() {
const text = sprintf(
@@ -132,13 +134,39 @@ export default {
return text;
},
+ approveUsersModal() {
+ const { pendingUserCount } = this;
+
+ return {
+ id: 'signup-settings-modal',
+ text: n__(
+ 'ApplicationSettings|By making this change, you will automatically approve %d user with the pending approval status.',
+ 'ApplicationSettings|By making this change, you will automatically approve %d users with the pending approval status.',
+ pendingUserCount,
+ ),
+ actionPrimary: {
+ text: n__(
+ 'ApplicationSettings|Approve %d user',
+ 'ApplicationSettings|Approve %d users',
+ pendingUserCount,
+ ),
+ attributes: {
+ variant: 'confirm',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ title: s__('ApplicationSettings|Approve users in the pending approval status?'),
+ };
+ },
},
watch: {
showModal(value) {
if (value === true) {
- this.$refs[this.$options.modal.id].show();
+ this.$refs[this.approveUsersModal.id].show();
} else {
- this.$refs[this.$options.modal.id].hide();
+ this.$refs[this.approveUsersModal.id].hide();
}
},
},
@@ -196,22 +224,6 @@ export default {
afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign up text'),
afterSignUpTextGroupDescription: s__('ApplicationSettings|Markdown enabled'),
},
- modal: {
- id: 'signup-settings-modal',
- actionPrimary: {
- text: s__('ApplicationSettings|Approve users'),
- attributes: {
- variant: 'confirm',
- },
- },
- actionCancel: {
- text: __('Cancel'),
- },
- title: s__('ApplicationSettings|Approve all users in the pending approval status?'),
- text: s__(
- 'ApplicationSettings|By making this change, you will automatically approve all users in pending approval status.',
- ),
- },
};
</script>
@@ -403,15 +415,15 @@ export default {
</gl-button>
<gl-modal
- :ref="$options.modal.id"
- :modal-id="$options.modal.id"
- :action-cancel="$options.modal.actionCancel"
- :action-primary="$options.modal.actionPrimary"
- :title="$options.modal.title"
+ :ref="approveUsersModal.id"
+ :modal-id="approveUsersModal.id"
+ :action-cancel="approveUsersModal.actionCancel"
+ :action-primary="approveUsersModal.actionPrimary"
+ :title="approveUsersModal.title"
@primary="submitForm"
@hide="modalHideHandler"
>
- {{ $options.modal.text }}
+ {{ approveUsersModal.text }}
</gl-modal>
</form>
</template>
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
index bf27b1a81ff..4c312a008cb 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
@@ -8,34 +8,34 @@ export const HELPER_TEXT_SERVICE_PING_ENABLED = __(
'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.',
);
-function setHelperText(usagePingCheckbox) {
+function setHelperText(servicePingCheckbox) {
const helperTextId = document.getElementById('service_ping_features_helper_text');
- const usagePingFeaturesLabel = document.getElementById('service_ping_features_label');
+ const servicePingFeaturesLabel = document.getElementById('service_ping_features_label');
- const usagePingFeaturesCheckbox = document.getElementById(
+ const servicePingFeaturesCheckbox = document.getElementById(
'application_setting_usage_ping_features_enabled',
);
- helperTextId.textContent = usagePingCheckbox.checked
+ helperTextId.textContent = servicePingCheckbox.checked
? HELPER_TEXT_SERVICE_PING_ENABLED
: HELPER_TEXT_SERVICE_PING_DISABLED;
- usagePingFeaturesLabel.classList.toggle('gl-cursor-not-allowed', !usagePingCheckbox.checked);
+ servicePingFeaturesLabel.classList.toggle('gl-cursor-not-allowed', !servicePingCheckbox.checked);
- usagePingFeaturesCheckbox.disabled = !usagePingCheckbox.checked;
+ servicePingFeaturesCheckbox.disabled = !servicePingCheckbox.checked;
- if (!usagePingCheckbox.checked) {
- usagePingFeaturesCheckbox.disabled = true;
- usagePingFeaturesCheckbox.checked = false;
+ if (!servicePingCheckbox.checked) {
+ servicePingFeaturesCheckbox.disabled = true;
+ servicePingFeaturesCheckbox.checked = false;
}
}
export default function initSetHelperText() {
- const usagePingCheckbox = document.getElementById('application_setting_usage_ping_enabled');
+ const servicePingCheckbox = document.getElementById('application_setting_usage_ping_enabled');
- setHelperText(usagePingCheckbox);
- usagePingCheckbox.addEventListener('change', () => {
- setHelperText(usagePingCheckbox);
+ setHelperText(servicePingCheckbox);
+ servicePingCheckbox.addEventListener('change', () => {
+ setHelperText(servicePingCheckbox);
});
}
diff --git a/app/assets/javascripts/pages/admin/groups/edit/index.js b/app/assets/javascripts/pages/admin/groups/edit/index.js
index a4e5df559ff..01e03ed437d 100644
--- a/app/assets/javascripts/pages/admin/groups/edit/index.js
+++ b/app/assets/javascripts/pages/admin/groups/edit/index.js
@@ -1,3 +1,3 @@
import initFilePickers from '~/file_pickers';
-document.addEventListener('DOMContentLoaded', initFilePickers);
+initFilePickers();
diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js
index 69d219d29f7..86b80a0ba5b 100644
--- a/app/assets/javascripts/pages/admin/groups/show/index.js
+++ b/app/assets/javascripts/pages/admin/groups/show/index.js
@@ -1,23 +1,3 @@
-import Vue from 'vue';
import UsersSelect from '~/users_select';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-function mountRemoveMemberModal() {
- const el = document.querySelector('.js-remove-member-modal');
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render(createComponent) {
- return createComponent(RemoveMemberModal);
- },
- });
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- mountRemoveMemberModal();
-
- new UsersSelect(); // eslint-disable-line no-new
-});
+new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/integrations/overrides/index.js b/app/assets/javascripts/pages/admin/integrations/overrides/index.js
new file mode 100644
index 00000000000..b1504709144
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/integrations/overrides/index.js
@@ -0,0 +1,3 @@
+import initIntegrationOverrides from '~/integrations/overrides';
+
+initIntegrationOverrides();
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index ffccc1419a6..63b98f4143b 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -18,7 +18,7 @@ export default {
computed: {
text() {
return s__(
- 'AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.',
+ 'AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.',
);
},
},
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index 042ff7808f1..b07ca815f13 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,23 +1,5 @@
-import Vue from 'vue';
import NamespaceSelect from '~/namespace_select';
import ProjectsList from '~/projects_list';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-
-function mountRemoveMemberModal() {
- const el = document.querySelector('.js-remove-member-modal');
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render(createComponent) {
- return createComponent(RemoveMemberModal);
- },
- });
-}
-
-mountRemoveMemberModal();
new ProjectsList(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index d5563470394..f83111b6385 100644
--- a/app/assets/javascripts/pages/admin/runners/index/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
@@ -1,17 +1,3 @@
-import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
-import { FILTERED_SEARCH } from '~/pages/constants';
-import initFilteredSearch from '~/pages/search/init_filtered_search';
-import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
-import { initRunnerList } from '~/runner/runner_list';
+import { initAdminRunners } from '~/runner/admin_runners';
-initFilteredSearch({
- page: FILTERED_SEARCH.ADMIN_RUNNERS,
- filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
- useDefaultState: true,
-});
-
-initInstallRunner();
-
-if (gon.features?.runnerListViewVueUi) {
- initRunnerList();
-}
+initAdminRunners();
diff --git a/app/assets/javascripts/pages/admin/serverless/domains/index.js b/app/assets/javascripts/pages/admin/serverless/domains/index.js
index 5be466886a5..4fab7a1d9cb 100644
--- a/app/assets/javascripts/pages/admin/serverless/domains/index.js
+++ b/app/assets/javascripts/pages/admin/serverless/domains/index.js
@@ -1,19 +1,17 @@
import initSettingsPanels from '~/settings_panels';
-document.addEventListener('DOMContentLoaded', () => {
- // Initialize expandable settings panels
- initSettingsPanels();
+// Initialize expandable settings panels
+initSettingsPanels();
- const domainCard = document.querySelector('.js-domain-cert-show');
- const domainForm = document.querySelector('.js-domain-cert-inputs');
- const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
- const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
+const domainCard = document.querySelector('.js-domain-cert-show');
+const domainForm = document.querySelector('.js-domain-cert-inputs');
+const domainReplaceButton = document.querySelector('.js-domain-cert-replace-btn');
+const domainSubmitButton = document.querySelector('.js-serverless-domain-submit');
- if (domainReplaceButton && domainCard && domainForm) {
- domainReplaceButton.addEventListener('click', () => {
- domainCard.classList.add('hidden');
- domainForm.classList.remove('hidden');
- domainSubmitButton.removeAttribute('disabled');
- });
- }
-});
+if (domainReplaceButton && domainCard && domainForm) {
+ domainReplaceButton.addEventListener('click', () => {
+ domainCard.classList.add('hidden');
+ domainForm.classList.remove('hidden');
+ domainSubmitButton.removeAttribute('disabled');
+ });
+}
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index b099165e3f5..6c134e4fad6 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -4,14 +4,12 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
-document.addEventListener('DOMContentLoaded', () => {
- addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
+addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
- initFilteredSearch({
- page: FILTERED_SEARCH.MERGE_REQUESTS,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
- useDefaultState: true,
- });
-
- projectSelect();
+initFilteredSearch({
+ page: FILTERED_SEARCH.MERGE_REQUESTS,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
+
+projectSelect();
diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
index 38ddebe30d9..b526fce6f7b 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js
@@ -1,3 +1,3 @@
import projectSelect from '~/project_select';
-document.addEventListener('DOMContentLoaded', projectSelect);
+projectSelect();
diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js
index 3c7edbdd7c7..808fcce46df 100644
--- a/app/assets/javascripts/pages/explore/groups/index.js
+++ b/app/assets/javascripts/pages/explore/groups/index.js
@@ -2,7 +2,7 @@ import GroupsList from '~/groups_list';
import Landing from '~/landing';
import initGroupsList from '../../../groups';
-document.addEventListener('DOMContentLoaded', () => {
+function exploreGroups() {
new GroupsList(); // eslint-disable-line no-new
initGroupsList();
const landingElement = document.querySelector('.js-explore-groups-landing');
@@ -13,4 +13,6 @@ document.addEventListener('DOMContentLoaded', () => {
'explore_groups_landing_dismissed',
);
exploreGroupsLanding.toggle();
-});
+}
+
+exploreGroups();
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 13656ee9b16..0137ff87979 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { groupMemberRequestFormatter } from '~/groups/members/utils';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
@@ -11,21 +10,6 @@ import { initMembersApp } from '~/members';
import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-
-function mountRemoveMemberModal() {
- const el = document.querySelector('.js-remove-member-modal');
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render(createComponent) {
- return createComponent(RemoveMemberModal);
- },
- });
-}
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
@@ -71,7 +55,6 @@ initMembersApp(document.querySelector('.js-group-members-list-app'), {
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
-mountRemoveMemberModal();
initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
diff --git a/app/assets/javascripts/pages/groups/runners/index.js b/app/assets/javascripts/pages/groups/runners/index.js
new file mode 100644
index 00000000000..ca1a6bdab75
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/runners/index.js
@@ -0,0 +1,3 @@
+import { initGroupRunners } from '~/runner/group_runners';
+
+initGroupRunners();
diff --git a/app/assets/javascripts/pages/groups/settings/badges/index.js b/app/assets/javascripts/pages/groups/settings/badges/index.js
index 3f48e4f281e..9dcea737d51 100644
--- a/app/assets/javascripts/pages/groups/settings/badges/index.js
+++ b/app/assets/javascripts/pages/groups/settings/badges/index.js
@@ -5,6 +5,4 @@ import Translate from '~/vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
- mountBadgeSettings(GROUP_BADGE);
-});
+mountBadgeSettings(GROUP_BADGE);
diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js
index fd8c590eb4e..736add8dca3 100644
--- a/app/assets/javascripts/pages/help/index/index.js
+++ b/app/assets/javascripts/pages/help/index/index.js
@@ -2,7 +2,5 @@ import $ from 'jquery';
import docs from '~/docs/docs_bundle';
import VersionCheckImage from '~/version_check_image';
-document.addEventListener('DOMContentLoaded', () => {
- docs();
- VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
-});
+docs();
+VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
diff --git a/app/assets/javascripts/pages/help/ui/index.js b/app/assets/javascripts/pages/help/ui/index.js
index 709ca2f3828..9ccc9123506 100644
--- a/app/assets/javascripts/pages/help/ui/index.js
+++ b/app/assets/javascripts/pages/help/ui/index.js
@@ -1,3 +1,3 @@
import initUIKit from '~/ui_development_kit';
-document.addEventListener('DOMContentLoaded', initUIKit);
+initUIKit();
diff --git a/app/assets/javascripts/pages/import/bitbucket/status/index.js b/app/assets/javascripts/pages/import/bitbucket/status/index.js
index f450a2aac00..6e9c26bf930 100644
--- a/app/assets/javascripts/pages/import/bitbucket/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket/status/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { initStoreFromElement, initPropsFromElement } from '~/import_entities/import_projects';
import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue';
-document.addEventListener('DOMContentLoaded', () => {
+function importBitBucket() {
const mountElement = document.getElementById('import-projects-mount-element');
if (!mountElement) return undefined;
@@ -16,4 +16,6 @@ document.addEventListener('DOMContentLoaded', () => {
return createElement(BitbucketStatusTable, { attrs });
},
});
-});
+}
+
+importBitBucket();
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
index a6d748ce857..90eb423c7a7 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { initStoreFromElement, initPropsFromElement } from '~/import_entities/import_projects';
import BitbucketServerStatusTable from './components/bitbucket_server_status_table.vue';
-document.addEventListener('DOMContentLoaded', () => {
+function BitbucketServerStatus() {
const mountElement = document.getElementById('import-projects-mount-element');
if (!mountElement) return undefined;
@@ -19,4 +19,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-});
+}
+
+BitbucketServerStatus();
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
index 68d4c1f049f..86b80a0ba5b 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
@@ -1,3 +1,3 @@
import UsersSelect from '~/users_select';
-document.addEventListener('DOMContentLoaded', () => new UsersSelect());
+new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/import/fogbugz/status/index.js b/app/assets/javascripts/pages/import/fogbugz/status/index.js
index 98ddb8b3aa4..4c427b72372 100644
--- a/app/assets/javascripts/pages/import/fogbugz/status/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/status/index.js
@@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
-document.addEventListener('DOMContentLoaded', () => {
- const mountElement = document.getElementById('import-projects-mount-element');
+const mountElement = document.getElementById('import-projects-mount-element');
- mountImportProjectsTable(mountElement);
-});
+mountImportProjectsTable(mountElement);
diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js
index 98ddb8b3aa4..4c427b72372 100644
--- a/app/assets/javascripts/pages/import/gitea/status/index.js
+++ b/app/assets/javascripts/pages/import/gitea/status/index.js
@@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
-document.addEventListener('DOMContentLoaded', () => {
- const mountElement = document.getElementById('import-projects-mount-element');
+const mountElement = document.getElementById('import-projects-mount-element');
- mountImportProjectsTable(mountElement);
-});
+mountImportProjectsTable(mountElement);
diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js
index 98ddb8b3aa4..4c427b72372 100644
--- a/app/assets/javascripts/pages/import/github/status/index.js
+++ b/app/assets/javascripts/pages/import/github/status/index.js
@@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
-document.addEventListener('DOMContentLoaded', () => {
- const mountElement = document.getElementById('import-projects-mount-element');
+const mountElement = document.getElementById('import-projects-mount-element');
- mountImportProjectsTable(mountElement);
-});
+mountImportProjectsTable(mountElement);
diff --git a/app/assets/javascripts/pages/import/gitlab/status/index.js b/app/assets/javascripts/pages/import/gitlab/status/index.js
index 98ddb8b3aa4..4c427b72372 100644
--- a/app/assets/javascripts/pages/import/gitlab/status/index.js
+++ b/app/assets/javascripts/pages/import/gitlab/status/index.js
@@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
-document.addEventListener('DOMContentLoaded', () => {
- const mountElement = document.getElementById('import-projects-mount-element');
+const mountElement = document.getElementById('import-projects-mount-element');
- mountImportProjectsTable(mountElement);
-});
+mountImportProjectsTable(mountElement);
diff --git a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
index bb86f72b95b..870c14f99ae 100644
--- a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
+++ b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
@@ -1,3 +1,3 @@
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
-document.addEventListener('DOMContentLoaded', initGitLabImportProject);
+initGitLabImportProject();
diff --git a/app/assets/javascripts/pages/import/manifest/status/index.js b/app/assets/javascripts/pages/import/manifest/status/index.js
index 98ddb8b3aa4..4c427b72372 100644
--- a/app/assets/javascripts/pages/import/manifest/status/index.js
+++ b/app/assets/javascripts/pages/import/manifest/status/index.js
@@ -1,7 +1,5 @@
import mountImportProjectsTable from '~/import_entities/import_projects';
-document.addEventListener('DOMContentLoaded', () => {
- const mountElement = document.getElementById('import-projects-mount-element');
+const mountElement = document.getElementById('import-projects-mount-element');
- mountImportProjectsTable(mountElement);
-});
+mountImportProjectsTable(mountElement);
diff --git a/app/assets/javascripts/pages/jira_connect/branches/new/index.js b/app/assets/javascripts/pages/jira_connect/branches/new/index.js
new file mode 100644
index 00000000000..f8c3ec63f1f
--- /dev/null
+++ b/app/assets/javascripts/pages/jira_connect/branches/new/index.js
@@ -0,0 +1,3 @@
+import initJiraConnectBranches from '~/jira_connect/branches';
+
+initJiraConnectBranches();
diff --git a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
index e93def5323f..8d8534ec556 100644
--- a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
+++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
@@ -1,3 +1,3 @@
import { mount2faAuthentication } from '~/authentication/mount_2fa';
-document.addEventListener('DOMContentLoaded', mount2faAuthentication);
+mount2faAuthentication();
diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js
index e93def5323f..8d8534ec556 100644
--- a/app/assets/javascripts/pages/omniauth_callbacks/index.js
+++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js
@@ -1,3 +1,3 @@
import { mount2faAuthentication } from '~/authentication/mount_2fa';
-document.addEventListener('DOMContentLoaded', mount2faAuthentication);
+mount2faAuthentication();
diff --git a/app/assets/javascripts/pages/profiles/accounts/show/index.js b/app/assets/javascripts/pages/profiles/accounts/show/index.js
index 5350ef61184..3d400ed77f5 100644
--- a/app/assets/javascripts/pages/profiles/accounts/show/index.js
+++ b/app/assets/javascripts/pages/profiles/accounts/show/index.js
@@ -1,6 +1,6 @@
import { initClose2faSuccessMessage } from '~/authentication/two_factor_auth';
import initProfileAccount from '~/profile/account';
-document.addEventListener('DOMContentLoaded', initProfileAccount);
+initProfileAccount();
initClose2faSuccessMessage();
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
index 4214d5bffb2..1b291d9509d 100644
--- a/app/assets/javascripts/pages/profiles/keys/index.js
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -1,9 +1,9 @@
import initConfirmModal from '~/confirm_modal';
import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
-document.addEventListener('DOMContentLoaded', () => {
- initConfirmModal();
+initConfirmModal();
+function initSshKeyValidation() {
const input = document.querySelector('.js-add-ssh-key-validation-input');
if (!input) return;
@@ -18,4 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
confirmSubmit,
);
addSshKeyValidation.register();
-});
+}
+
+initSshKeyValidation();
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 186072531b8..50835333a54 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -2,17 +2,15 @@ import { mount2faRegistration } from '~/authentication/mount_2fa';
import { initRecoveryCodes } from '~/authentication/two_factor_auth';
import { parseBoolean } from '~/lib/utils/common_utils';
-document.addEventListener('DOMContentLoaded', () => {
- const twoFactorNode = document.querySelector('.js-two-factor-auth');
- const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSkippable) : false;
+const twoFactorNode = document.querySelector('.js-two-factor-auth');
+const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSkippable) : false;
- if (skippable) {
- const button = `<a class="btn btn-sm btn-warning float-right" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
- const flashAlert = document.querySelector('.flash-alert');
- if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
- }
+if (skippable) {
+ const button = `<a class="btn btn-sm btn-warning float-right" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`;
+ const flashAlert = document.querySelector('.flash-alert');
+ if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button);
+}
- mount2faRegistration();
-});
+mount2faRegistration();
initRecoveryCodes();
diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js
index 057ef157374..07ee4d686cc 100644
--- a/app/assets/javascripts/pages/projects/artifacts/file/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js
@@ -1,5 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import BlobViewer from '~/blob/viewer/index';
+import { BlobViewer } from '~/blob/viewer/index';
new ShortcutsNavigation(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 6cc0095f5a5..b365e039191 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import TableOfContents from '~/blob/components/table_contents.vue';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
-import BlobViewer from '~/blob/viewer/index';
+import { BlobViewer, initAuxiliaryViewer } from '~/blob/viewer/index';
import GpgBadges from '~/gpg_badges';
import createDefaultClient from '~/lib/graphql';
import initBlob from '~/pages/projects/init_blob';
@@ -39,6 +39,8 @@ if (viewBlobEl) {
});
},
});
+
+ initAuxiliaryViewer();
} else {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 549e596cb8d..5edaa7f7e51 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -5,9 +5,7 @@ import initCompareSelector from '~/projects/compare';
initCompareSelector();
-document.addEventListener('DOMContentLoaded', () => {
- new Diff(); // eslint-disable-line no-new
- const paddingTop = 16;
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
- GpgBadges.fetch();
-});
+new Diff(); // eslint-disable-line no-new
+const paddingTop = 16;
+initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+GpgBadges.fetch();
diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
index 255d05b39be..bef21ef8fdf 100644
--- a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
+++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
@@ -1,3 +1,3 @@
import initCycleAnalytics from '~/cycle_analytics';
-document.addEventListener('DOMContentLoaded', initCycleAnalytics);
+initCycleAnalytics();
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 9aa7e62e3ee..335d8d481fc 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -25,10 +25,6 @@ initProjectLoadingSpinner();
initProjectPermissionsSettings();
setupTransferEdit('.js-project-transfer-form', 'select.select2');
-dirtySubmitFactory(
- document.querySelectorAll(
- '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form',
- ),
-);
+dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr-settings-form'));
initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/environments/edit/index.js b/app/assets/javascripts/pages/projects/environments/edit/index.js
new file mode 100644
index 00000000000..574963d825a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/edit/index.js
@@ -0,0 +1,3 @@
+import mountEdit from '~/environments/edit';
+
+mountEdit(document.getElementById('js-edit-environment'));
diff --git a/app/assets/javascripts/pages/projects/environments/folder/index.js b/app/assets/javascripts/pages/projects/environments/folder/index.js
index 5feaf944038..2f22a3a84ff 100644
--- a/app/assets/javascripts/pages/projects/environments/folder/index.js
+++ b/app/assets/javascripts/pages/projects/environments/folder/index.js
@@ -1,3 +1,3 @@
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
-document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
+initEnvironmentsFolderBundle();
diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js
index d3028aec313..606439866ea 100644
--- a/app/assets/javascripts/pages/projects/environments/metrics/index.js
+++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js
@@ -1,3 +1,3 @@
import monitoringApp from '~/monitoring/monitoring_app';
-document.addEventListener('DOMContentLoaded', monitoringApp);
+monitoringApp();
diff --git a/app/assets/javascripts/pages/projects/environments/new/index.js b/app/assets/javascripts/pages/projects/environments/new/index.js
new file mode 100644
index 00000000000..2edb1ca7088
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/new/index.js
@@ -0,0 +1,3 @@
+import mountNew from '~/environments/new';
+
+mountNew(document.getElementById('js-new-environment'));
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index a4960037eaa..53e48ad8d86 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,3 +1,5 @@
-import initShowEnvironment from '~/environments/mount_show';
+import initConfirmRollBackModal from '~/environments/init_confirm_rollback_modal';
+import { initHeader } from '~/environments/mount_show';
-initShowEnvironment();
+initHeader();
+initConfirmRollBackModal();
diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js
index 7129e24cee1..d42c163a41b 100644
--- a/app/assets/javascripts/pages/projects/environments/terminal/index.js
+++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js
@@ -1,3 +1,3 @@
import initTerminal from '~/terminal/';
-document.addEventListener('DOMContentLoaded', initTerminal);
+initTerminal();
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index ea38b8e15a4..c217bc5a727 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -7,151 +7,149 @@ import SeriesDataMixin from './series_data_mixin';
const seriesDataToBarData = (raw) => Object.entries(raw).map(([name, data]) => ({ name, data }));
-document.addEventListener('DOMContentLoaded', () => {
- waitForCSSLoaded(() => {
- const languagesContainer = document.getElementById('js-languages-chart');
- const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
- const monthContainer = document.getElementById('js-month-chart');
- const weekdayContainer = document.getElementById('js-weekday-chart');
- const hourContainer = document.getElementById('js-hour-chart');
- const LANGUAGE_CHART_HEIGHT = 300;
- const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
- if (firstDayOfWeek === 0) {
- return weekDays;
- }
+waitForCSSLoaded(() => {
+ const languagesContainer = document.getElementById('js-languages-chart');
+ const codeCoverageContainer = document.getElementById('js-code-coverage-chart');
+ const monthContainer = document.getElementById('js-month-chart');
+ const weekdayContainer = document.getElementById('js-weekday-chart');
+ const hourContainer = document.getElementById('js-hour-chart');
+ const LANGUAGE_CHART_HEIGHT = 300;
+ const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
+ if (firstDayOfWeek === 0) {
+ return weekDays;
+ }
- return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
- const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
+ return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => {
+ const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length];
- return {
- ...acc,
- [reorderedDayName]: weekDays[reorderedDayName],
- };
- }, {});
- };
+ return {
+ ...acc,
+ [reorderedDayName]: weekDays[reorderedDayName],
+ };
+ }, {});
+ };
- // eslint-disable-next-line no-new
- new Vue({
- el: languagesContainer,
- components: {
- GlColumnChart,
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
},
- data() {
- return {
- chartData: JSON.parse(languagesContainer.dataset.chartData),
- };
- },
- computed: {
- seriesData() {
- return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }];
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Used programming language'),
- yAxisTitle: __('Percentage'),
- xAxisType: 'category',
- },
- attrs: {
- height: LANGUAGE_CHART_HEIGHT,
- },
- });
- },
- });
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: codeCoverageContainer,
- render(h) {
- return h(CodeCoverage, {
- props: {
- graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: codeCoverageContainer,
+ render(h) {
+ return h(CodeCoverage, {
+ props: {
+ graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint,
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: monthContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(monthContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Day of month'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: weekdayContainer,
- components: {
- GlColumnChart,
- },
- data() {
- return {
- chartData: JSON.parse(weekdayContainer.dataset.chartData),
- };
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return [{ name: 'full', data }];
},
- computed: {
- seriesData() {
- const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
- const data = Object.keys(weekDays).reduce((acc, key) => {
- acc.push([key, weekDays[key]]);
- return acc;
- }, []);
- return [{ name: 'full', data }];
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
},
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: this.seriesData,
- xAxisTitle: __('Weekday'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
- });
+ });
+ },
+ });
- // eslint-disable-next-line no-new
- new Vue({
- el: hourContainer,
- components: {
- GlColumnChart,
- },
- mixins: [SeriesDataMixin],
- data() {
- return {
- chartData: JSON.parse(hourContainer.dataset.chartData),
- };
- },
- render(h) {
- return h(GlColumnChart, {
- props: {
- bars: seriesDataToBarData(this.seriesData),
- xAxisTitle: __('Hour (UTC)'),
- yAxisTitle: __('No. of commits'),
- xAxisType: 'category',
- },
- });
- },
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ bars: seriesDataToBarData(this.seriesData),
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
});
});
diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js
index 09d9c78c446..4f5a5bfe6fe 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/index.js
@@ -1,3 +1,3 @@
import initContributorsGraphs from '~/contributors';
-document.addEventListener('DOMContentLoaded', initContributorsGraphs);
+initContributorsGraphs();
diff --git a/app/assets/javascripts/pages/projects/import/jira/index.js b/app/assets/javascripts/pages/projects/import/jira/index.js
index cb7a7bde55d..5876e5283b5 100644
--- a/app/assets/javascripts/pages/projects/import/jira/index.js
+++ b/app/assets/javascripts/pages/projects/import/jira/index.js
@@ -1,3 +1,3 @@
import mountJiraImportApp from '~/jira_import';
-document.addEventListener('DOMContentLoaded', mountJiraImportApp);
+mountJiraImportApp();
diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js
index aecc6484b26..48afd2142ee 100644
--- a/app/assets/javascripts/pages/projects/issues/edit/index.js
+++ b/app/assets/javascripts/pages/projects/issues/edit/index.js
@@ -1,3 +1,3 @@
import initForm from 'ee_else_ce/pages/projects/issues/form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 3cea61262ea..e365f51567d 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,10 +3,10 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
-import '~/notes/index';
import initIncidentApp from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
+import initNotesApp from '~/notes/index';
import { store } from '~/notes/stores';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
@@ -14,6 +14,8 @@ import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_iss
import ZenMode from '~/zen_mode';
export default function initShowIssue() {
+ initNotesApp();
+
const initialDataEl = document.getElementById('js-issuable-app');
const { issueType, ...issuableData } = parseIssuableData(initialDataEl);
diff --git a/app/assets/javascripts/pages/projects/jobs/terminal/index.js b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
index 7129e24cee1..d42c163a41b 100644
--- a/app/assets/javascripts/pages/projects/jobs/terminal/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/terminal/index.js
@@ -1,3 +1,3 @@
import initTerminal from '~/terminal/';
-document.addEventListener('DOMContentLoaded', initTerminal);
+initTerminal();
diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js
index 9f05f63b742..2dabcfadfab 100644
--- a/app/assets/javascripts/pages/projects/network/show/index.js
+++ b/app/assets/javascripts/pages/projects/network/show/index.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
import Network from '../network';
-document.addEventListener('DOMContentLoaded', () => {
+(() => {
if (!$('.network-graph').length) return;
const networkGraph = new Network({
@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new ShortcutsNetwork(networkGraph.branch_graph);
-});
+})();
diff --git a/app/assets/javascripts/pages/projects/pages_domains/new/index.js b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
index 27e4433ad4d..17fa49a46e0 100644
--- a/app/assets/javascripts/pages/projects/pages_domains/new/index.js
+++ b/app/assets/javascripts/pages/projects/pages_domains/new/index.js
@@ -1,3 +1,3 @@
import initForm from '~/pages/projects/pages_domains/form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/pages_domains/show/index.js b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
index 27e4433ad4d..17fa49a46e0 100644
--- a/app/assets/javascripts/pages/projects/pages_domains/show/index.js
+++ b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
@@ -1,3 +1,3 @@
import initForm from '~/pages/projects/pages_domains/form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 40730ec7e60..cd4bc35e74e 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-document.addEventListener('DOMContentLoaded', () => {
+function initPipelineSchedules() {
const el = document.getElementById('pipeline-schedules-callout');
if (!el) {
@@ -21,4 +21,6 @@ document.addEventListener('DOMContentLoaded', () => {
return createElement(PipelineSchedulesCallout);
},
});
-});
+}
+
+initPipelineSchedules();
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 177dc346c60..fb0be31834d 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import groupsSelect from '~/groups_select';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersForm from '~/invite_members/init_invite_members_form';
@@ -11,26 +10,10 @@ import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select';
-import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
-
-function mountRemoveMemberModal() {
- const el = document.querySelector('.js-remove-member-modal');
- if (!el) {
- return false;
- }
-
- return new Vue({
- el,
- render(createComponent) {
- return createComponent(RemoveMemberModal);
- },
- });
-}
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
-mountRemoveMemberModal();
initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
diff --git a/app/assets/javascripts/pages/projects/security/configuration/index.js b/app/assets/javascripts/pages/projects/security/configuration/index.js
index 8bba3d7af54..5f801501b2f 100644
--- a/app/assets/javascripts/pages/projects/security/configuration/index.js
+++ b/app/assets/javascripts/pages/projects/security/configuration/index.js
@@ -1,3 +1,3 @@
-import { initCESecurityConfiguration } from '~/security_configuration';
+import { initSecurityConfiguration } from '~/security_configuration';
-initCESecurityConfiguration(document.querySelector('#js-security-configuration-static'));
+initSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 8e603c5c1a2..03ffc323fc0 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -2,16 +2,14 @@ import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
-document.addEventListener('DOMContentLoaded', () => {
- const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
+const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+integrationSettingsForm.init();
- const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
- const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
- if (prometheusSettingsWrapper) {
- const customMetrics = new CustomMetrics(prometheusSettingsSelector);
- customMetrics.init();
- }
+const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
+const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
+if (prometheusSettingsWrapper) {
+ const customMetrics = new CustomMetrics(prometheusSettingsSelector);
+ customMetrics.init();
+}
- PrometheusAlerts();
-});
+PrometheusAlerts();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 6fcaa3ab04b..261f7af7ef1 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -3,7 +3,6 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui'
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
visibilityOptions,
visibilityLevelDescriptions,
@@ -12,6 +11,7 @@ import {
featureAccessLevel,
featureAccessLevelNone,
CVE_ID_REQUEST_BUTTON_I18N,
+ featureAccessLevelDescriptions,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
import projectFeatureSetting from './project_feature_setting.vue';
@@ -48,7 +48,7 @@ export default {
GlFormCheckbox,
GlToggle,
},
- mixins: [settingsMixin, glFeatureFlagsMixin()],
+ mixins: [settingsMixin],
props: {
requestCveAvailable: {
@@ -177,7 +177,7 @@ export default {
requirementsAccessLevel: featureAccessLevel.EVERYONE,
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
operationsAccessLevel: featureAccessLevel.EVERYONE,
- containerRegistryEnabled: true,
+ containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
lfsEnabled: true,
requestAccessEnabled: true,
highlightChangesClass: false,
@@ -185,6 +185,8 @@ export default {
cveIdRequestEnabled: true,
featureAccessLevelEveryone,
featureAccessLevelMembers,
+ featureAccessLevel,
+ featureAccessLevelDescriptions,
};
return { ...defaults, ...this.currentSettings };
@@ -249,7 +251,10 @@ export default {
},
showContainerRegistryPublicNote() {
- return this.visibilityLevel === visibilityOptions.PUBLIC;
+ return (
+ this.visibilityLevel === visibilityOptions.PUBLIC &&
+ this.containerRegistryAccessLevel === featureAccessLevel.EVERYONE
+ );
},
repositoryHelpText() {
@@ -311,6 +316,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.operationsAccessLevel,
);
+ this.containerRegistryAccessLevel = Math.min(
+ featureAccessLevel.PROJECT_MEMBERS,
+ this.containerRegistryAccessLevel,
+ );
if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS;
@@ -340,6 +349,8 @@ export default {
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.operationsAccessLevel = featureAccessLevel.EVERYONE;
+ if (this.containerRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
+ this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE;
this.highlightChanges();
}
@@ -513,31 +524,6 @@ export default {
/>
</project-setting-row>
<project-setting-row
- v-if="registryAvailable"
- ref="container-registry-settings"
- :help-path="registryHelpPath"
- :label="$options.i18n.containerRegistryLabel"
- :help-text="
- s__('ProjectSettings|Every project can have its own space to store its Docker images')
- "
- >
- <div v-if="showContainerRegistryPublicNote" class="text-muted">
- {{
- s__(
- 'ProjectSettings|Note: the container registry is always visible when a project is public',
- )
- }}
- </div>
- <gl-toggle
- v-model="containerRegistryEnabled"
- class="gl-my-2"
- :disabled="!repositoryEnabled"
- :label="$options.i18n.containerRegistryLabel"
- label-position="hidden"
- name="project[container_registry_enabled]"
- />
- </project-setting-row>
- <project-setting-row
v-if="lfsAvailable"
ref="git-lfs-settings"
:help-path="lfsHelpPath"
@@ -590,18 +576,47 @@ export default {
name="project[packages_enabled]"
/>
</project-setting-row>
+ <project-setting-row
+ ref="pipeline-settings"
+ :label="$options.i18n.ciCdLabel"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
+ >
+ <project-feature-setting
+ v-model="buildsAccessLevel"
+ :label="$options.i18n.ciCdLabel"
+ :options="repoFeatureAccessLevelOptions"
+ :disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][builds_access_level]"
+ />
+ </project-setting-row>
</div>
<project-setting-row
- ref="pipeline-settings"
- :label="$options.i18n.ciCdLabel"
- :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
+ v-if="registryAvailable"
+ ref="container-registry-settings"
+ :help-path="registryHelpPath"
+ :label="$options.i18n.containerRegistryLabel"
+ :help-text="
+ s__('ProjectSettings|Every project can have its own space to store its Docker images')
+ "
>
+ <div v-if="showContainerRegistryPublicNote" class="text-muted">
+ <gl-sprintf
+ :message="
+ s__(
+ `ProjectSettings|Note: The container registry is always visible when a project is public and the container registry is set to '%{access_level_description}'`,
+ )
+ "
+ >
+ <template #access_level_description>{{
+ featureAccessLevelDescriptions[featureAccessLevel.EVERYONE]
+ }}</template>
+ </gl-sprintf>
+ </div>
<project-feature-setting
- v-model="buildsAccessLevel"
- :label="$options.i18n.ciCdLabel"
- :options="repoFeatureAccessLevelOptions"
- :disabled-input="!repositoryEnabled"
- name="project[project_feature_attributes][builds_access_level]"
+ v-model="containerRegistryAccessLevel"
+ :options="featureAccessLevelOptions"
+ :label="$options.i18n.containerRegistryLabel"
+ name="project[project_feature_attributes][container_registry_access_level]"
/>
</project-setting-row>
<project-setting-row
@@ -737,22 +752,5 @@ export default {
}}</template>
</gl-form-checkbox>
</project-setting-row>
- <project-setting-row
- v-if="glFeatures.allowEditingCommitMessages"
- ref="allow-editing-commit-messages"
- class="gl-mb-4"
- >
- <input
- :value="allowEditingCommitMessages"
- type="hidden"
- name="project[project_setting_attributes][allow_editing_commit_messages]"
- />
- <gl-form-checkbox v-model="allowEditingCommitMessages">
- {{ s__('ProjectSettings|Allow editing commit messages') }}
- <template #help>{{
- s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.')
- }}</template>
- </gl-form-checkbox>
- </project-setting-row>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
index e160fdacca6..fb1acd5311c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
@@ -22,7 +22,7 @@ export const featureAccessLevel = {
EVERYONE: 20,
};
-const featureAccessLevelDescriptions = {
+export const featureAccessLevelDescriptions = {
[featureAccessLevel.NOT_ENABLED]: __('Enable feature to choose access level'),
[featureAccessLevel.PROJECT_MEMBERS]: __('Only Project Members'),
[featureAccessLevel.EVERYONE]: __('Everyone With Access'),
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 26f8018a968..78b3f2f1b30 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,7 +1,7 @@
import initTree from 'ee_else_ce/repository';
import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import BlobViewer from '~/blob/viewer/index';
+import { BlobViewer } from '~/blob/viewer/index';
import { initUploadForm } from '~/blob_edit/blob_bundle';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
diff --git a/app/assets/javascripts/pages/projects/static_site_editor/show/index.js b/app/assets/javascripts/pages/projects/static_site_editor/show/index.js
index 8f808dae56c..d9d265e4e4a 100644
--- a/app/assets/javascripts/pages/projects/static_site_editor/show/index.js
+++ b/app/assets/javascripts/pages/projects/static_site_editor/show/index.js
@@ -1,5 +1,3 @@
import initStaticSiteEditor from '~/static_site_editor';
-window.addEventListener('DOMContentLoaded', () => {
- initStaticSiteEditor(document.querySelector('#static-site-editor'));
-});
+initStaticSiteEditor(document.querySelector('#static-site-editor'));
diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js
index e93def5323f..8d8534ec556 100644
--- a/app/assets/javascripts/pages/sessions/index.js
+++ b/app/assets/javascripts/pages/sessions/index.js
@@ -1,3 +1,3 @@
import { mount2faAuthentication } from '~/authentication/mount_2fa';
-document.addEventListener('DOMContentLoaded', mount2faAuthentication);
+mount2faAuthentication();
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 465aed88c01..8c2fd624a83 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -7,18 +7,16 @@ import preserveUrlFragment from './preserve_url_fragment';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import UsernameValidator from './username_validator';
-document.addEventListener('DOMContentLoaded', () => {
- new UsernameValidator(); // eslint-disable-line no-new
- new LengthValidator(); // eslint-disable-line no-new
- new SigninTabsMemoizer(); // eslint-disable-line no-new
- new NoEmojiValidator(); // eslint-disable-line no-new
+new UsernameValidator(); // eslint-disable-line no-new
+new LengthValidator(); // eslint-disable-line no-new
+new SigninTabsMemoizer(); // eslint-disable-line no-new
+new NoEmojiValidator(); // eslint-disable-line no-new
- new OAuthRememberMe({
- container: $('.omniauth-container'),
- }).bindEvents();
+new OAuthRememberMe({
+ container: $('.omniauth-container'),
+}).bindEvents();
- // Save the URL fragment from the current window location. This will be present if the user was
- // redirected to sign-in after attempting to access a protected URL that included a fragment.
- preserveUrlFragment(window.location.hash);
- initVueAlerts();
-});
+// Save the URL fragment from the current window location. This will be present if the user was
+// redirected to sign-in after attempting to access a protected URL that included a fragment.
+preserveUrlFragment(window.location.hash);
+initVueAlerts();
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 e883fecb170..a8ec731e105 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -6,7 +6,6 @@ import {
GlButton,
GlSprintf,
GlAlert,
- GlLoadingIcon,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
@@ -114,7 +113,6 @@ export default {
GlButton,
GlModal,
MarkdownField,
- GlLoadingIcon,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
@@ -134,14 +132,14 @@ export default {
isContentEditorLoading: true,
useContentEditor: false,
commitMessage: '',
- contentEditor: null,
isDirty: false,
contentEditorRenderFailed: false,
+ contentEditorEmpty: false,
};
},
computed: {
noContent() {
- if (this.isContentEditorActive) return this.contentEditor?.empty;
+ if (this.isContentEditorActive) return this.contentEditorEmpty;
return !this.content.trim();
},
csrfToken() {
@@ -206,7 +204,7 @@ export default {
window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
- getContentHTML(content) {
+ renderMarkdown(content) {
return axios
.post(this.pageInfo.markdownPreviewPath, { text: content })
.then(({ data }) => data.body);
@@ -233,6 +231,32 @@ export default {
this.isDirty = true;
},
+ async loadInitialContent(contentEditor) {
+ this.contentEditor = contentEditor;
+
+ try {
+ await this.contentEditor.setSerializedContent(this.content);
+ this.trackContentEditorLoaded();
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ async retryInitContentEditor() {
+ try {
+ this.contentEditorRenderFailed = false;
+ await this.contentEditor.setSerializedContent(this.content);
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ handleContentEditorChange({ empty }) {
+ this.contentEditorEmpty = empty;
+ // TODO: Implement a precise mechanism to detect changes in the Content
+ this.isDirty = true;
+ },
+
onPageUnload(event) {
if (!this.isDirty) return undefined;
@@ -253,36 +277,8 @@ export default {
this.commitMessage = newCommitMessage;
},
- async initContentEditor() {
- this.isContentEditorLoading = true;
+ initContentEditor() {
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),
- uploadsPath: this.pageInfo.uploadsPath,
- tiptapOptions: {
- onUpdate: () => this.handleContentChange(),
- },
- });
-
- try {
- await this.contentEditor.setSerializedContent(this.content);
- this.isContentEditorLoading = false;
-
- this.trackContentEditorLoaded();
- } catch (e) {
- this.contentEditorRenderFailed = true;
- }
- },
-
- retryInitContentEditor() {
- this.contentEditorRenderFailed = false;
- this.initContentEditor();
},
switchToOldEditor() {
@@ -401,6 +397,7 @@ export default {
v-if="showContentEditorAlert"
class="gl-mb-6"
variant="info"
+ data-qa-selector="try_new_editor_container"
:primary-button-text="$options.i18n.contentEditor.useNewEditor.primaryLabel"
:secondary-button-text="$options.i18n.contentEditor.useNewEditor.secondaryLabel"
:dismiss-label="$options.i18n.contentEditor.useNewEditor.secondaryLabel"
@@ -476,12 +473,12 @@ export default {
>
</gl-sprintf>
</gl-alert>
- <gl-loading-icon
- v-if="isContentEditorLoading"
- size="sm"
- class="bordered-box gl-w-full gl-py-6"
+ <content-editor
+ :render-markdown="renderMarkdown"
+ :uploads-path="pageInfo.uploadsPath"
+ @initialized="loadInitialContent"
+ @change="handleContentEditorChange"
/>
- <content-editor v-else :content-editor="contentEditor" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
</div>
diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js
index b9a9ef215af..28a4257c0c3 100644
--- a/app/assets/javascripts/performance/constants.js
+++ b/app/assets/javascripts/performance/constants.js
@@ -89,3 +89,14 @@ export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish';
// Measures
export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the viewer';
export const REPO_BLOB_SWITCH_VIEWER = 'Repository File Viewer: switching the viewer';
+
+//
+// DESIGN MANAGEMENT NAMESPACE
+//
+
+// Marks
+export const DESIGN_MARK_APP_START = 'design-app-start';
+
+// Measures
+export const DESIGN_MEASURE_BEFORE_APP = 'Design Management: Before the Vue app';
+export const DESIGN_MAIN_IMAGE_OUTPUT = 'Design Management: Single image preview';
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 214e1729bf8..670b0535ca3 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -40,7 +40,7 @@ export default {
metric: 'active-record',
title: 'pg',
header: s__('PerformanceBar|SQL queries'),
- keys: ['sql', 'cached', 'transaction', 'db_role'],
+ keys: ['sql', 'cached', 'transaction', 'db_role', 'db_config_name'],
},
{
metric: 'bullet',
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index cadcab16f16..8170a1f8443 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -7,7 +7,6 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
- '.js-service-templates-deprecated-callout',
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
];
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 ee6d4ff7c4d..9a6eed50fbe 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
@@ -212,6 +212,7 @@ export default {
:text="currentBranch"
icon="branch"
data-qa-selector="branch_selector_button"
+ data-testid="branch-selector"
>
<gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
<gl-dropdown-section-header>
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 6af3361e7e6..46f6f4a28c1 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
@@ -19,12 +19,14 @@ export const i18n = {
pipelineInfo: s__(
`Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}`,
),
+ viewBtn: s__('Pipeline|View pipeline'),
};
export default {
i18n,
components: {
CiIcon,
+ GlButton,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -98,44 +100,63 @@ export default {
</script>
<template>
- <div class="gl-white-space-nowrap gl-max-w-full">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full"
+ >
<template v-if="showLoadingState">
- <gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
- <span data-testid="pipeline-loading-msg">{{ $options.i18n.fetchLoading }}</span>
+ <div>
+ <gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
+ <span data-testid="pipeline-loading-msg">{{ $options.i18n.fetchLoading }}</span>
+ </div>
</template>
<template v-else-if="hasError">
- <gl-icon class="gl-mr-auto" name="warning-solid" />
- <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ <div>
+ <gl-icon class="gl-mr-auto" name="warning-solid" />
+ <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ </div>
</template>
<template v-else>
- <a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" />
- </a>
- <span class="gl-font-weight-bold">
- <gl-sprintf :message="$options.i18n.pipelineInfo">
- <template #id="{ content }">
- <gl-link
- :href="status.detailsPath"
- class="pipeline-id gl-font-weight-normal pipeline-number"
- target="_blank"
- data-testid="pipeline-id"
- >
- {{ content }}{{ pipelineId }}</gl-link
- >
- </template>
- <template #status>{{ status.text }}</template>
- <template #commit>
- <gl-link
- :href="pipeline.commitPath"
- class="commit-sha gl-font-weight-normal"
- target="_blank"
- data-testid="pipeline-commit"
- >
- {{ shortSha }}
- </gl-link>
- </template>
- </gl-sprintf>
- </span>
+ <div>
+ <a :href="status.detailsPath" class="gl-mr-auto">
+ <ci-icon :status="status" :size="16" />
+ </a>
+ <span class="gl-font-weight-bold">
+ <gl-sprintf :message="$options.i18n.pipelineInfo">
+ <template #id="{ content }">
+ <gl-link
+ :href="status.detailsPath"
+ class="pipeline-id gl-font-weight-normal pipeline-number"
+ target="_blank"
+ data-testid="pipeline-id"
+ >
+ {{ content }}{{ pipelineId }}</gl-link
+ >
+ </template>
+ <template #status>{{ status.text }}</template>
+ <template #commit>
+ <gl-link
+ :href="pipeline.commitPath"
+ class="commit-sha gl-font-weight-normal"
+ target="_blank"
+ data-testid="pipeline-commit"
+ >
+ {{ shortSha }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ <div>
+ <gl-button
+ target="_blank"
+ category="secondary"
+ variant="confirm"
+ :href="status.detailsPath"
+ data-testid="pipeline-view-btn"
+ >
+ {{ $options.i18n.viewBtn }}
+ </gl-button>
+ </div>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ea45b5e3ec7..015f0519c72 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -39,10 +39,10 @@ export default {
required: false,
default: false,
},
- pipelineLayers: {
- type: Array,
+ computedPipelineInfo: {
+ type: Object,
required: false,
- default: () => [],
+ default: () => ({}),
},
type: {
type: String,
@@ -81,7 +81,10 @@ export default {
layout() {
return this.isStageView
? this.pipeline.stages
- : generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers);
+ : generateColumnsFromLayersListMemoized(
+ this.pipeline,
+ this.computedPipelineInfo.pipelineLayers,
+ );
},
hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0);
@@ -92,6 +95,9 @@ export default {
isStageView() {
return this.viewType === STAGE_VIEW;
},
+ linksData() {
+ return this.computedPipelineInfo?.linksData ?? null;
+ },
metricsConfig() {
return {
path: this.configPaths.metricsPath,
@@ -188,6 +194,7 @@ export default {
:container-id="containerId"
:container-measurements="measurements"
:highlighted-job="hoveredJobName"
+ :links-data="linksData"
:metrics-config="metricsConfig"
:show-links="showJobLinks"
:view-type="viewType"
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
deleted file mode 100644
index 39d0fa8a8ca..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ /dev/null
@@ -1,269 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { escape, capitalize } from 'lodash';
-import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
-import { reportToSentry } from '../../utils';
-import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
-import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
-import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
-
-export default {
- name: 'PipelineGraphLegacy',
- components: {
- GlLoadingIcon,
- LinkedPipelinesColumnLegacy,
- StageColumnComponentLegacy,
- },
- mixins: [GraphBundleMixin],
- props: {
- isLoading: {
- type: Boolean,
- required: true,
- },
- pipeline: {
- type: Object,
- required: true,
- },
- isLinkedPipeline: {
- type: Boolean,
- required: false,
- default: false,
- },
- mediator: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: false,
- default: MAIN,
- },
- },
- upstream: UPSTREAM,
- downstream: DOWNSTREAM,
- data() {
- return {
- downstreamMarginTop: null,
- jobName: null,
- pipelineExpanded: {
- jobName: '',
- expanded: false,
- },
- };
- },
- computed: {
- graph() {
- return this.pipeline.details?.stages;
- },
- hasUpstream() {
- return (
- this.type !== this.$options.downstream &&
- this.upstreamPipelines &&
- this.pipeline.triggered_by !== null
- );
- },
- upstreamPipelines() {
- return this.pipeline.triggered_by;
- },
- hasDownstream() {
- return (
- this.type !== this.$options.upstream &&
- this.downstreamPipelines &&
- this.pipeline.triggered.length > 0
- );
- },
- downstreamPipelines() {
- return this.pipeline.triggered;
- },
- expandedUpstream() {
- return (
- this.pipeline.triggered_by &&
- Array.isArray(this.pipeline.triggered_by) &&
- this.pipeline.triggered_by.find((el) => el.isExpanded)
- );
- },
- expandedDownstream() {
- return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded);
- },
- pipelineTypeUpstream() {
- return this.type !== this.$options.downstream && this.expandedUpstream;
- },
- pipelineTypeDownstream() {
- return this.type !== this.$options.upstream && this.expandedDownstream;
- },
- pipelineProjectId() {
- return this.pipeline.project.id;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
- },
- methods: {
- capitalizeStageName(name) {
- const escapedName = escape(name);
- return capitalize(escapedName);
- },
- isFirstColumn(index) {
- return index === 0;
- },
- stageConnectorClass(index, stage) {
- let className;
-
- // If it's the first stage column and only has one job
- if (this.isFirstColumn(index) && stage.groups.length === 1) {
- className = 'no-margin';
- } else if (index > 0) {
- // If it is not the first column
- className = 'left-margin';
- }
-
- return className;
- },
- refreshPipelineGraph() {
- this.$emit('refreshPipelineGraph');
- },
- /**
- * CSS class is applied:
- * - if pipeline graph contains only one stage column component
- *
- * @param {number} index
- * @returns {boolean}
- */
- shouldAddRightMargin(index) {
- return !(index === this.graph.length - 1);
- },
- handleClickedDownstream(pipeline, clickedIndex, downstreamNode) {
- /**
- * Calculates the margin top of the clicked downstream pipeline by
- * subtracting the clicked downstream pipelines offsetTop by it's parent's
- * offsetTop and then subtracting 15
- */
- this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
-
- /**
- * If the expanded trigger is defined and the id is different than the
- * pipeline we clicked, then it means we clicked on a sibling downstream link
- * and we want to reset the pipeline store. Triggering the reset without
- * this condition would mean not allowing downstreams of downstreams to expand
- */
- if (this.expandedDownstream?.id !== pipeline.id) {
- this.$emit('onResetDownstream', this.pipeline, pipeline);
- }
-
- this.$emit('onClickDownstreamPipeline', pipeline);
- },
- calculateMarginTop(downstreamNode, pixelDiff) {
- return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`;
- },
- hasOnlyOneJob(stage) {
- return stage.groups.length === 1;
- },
- hasUpstreamColumn(index) {
- return index === 0 && this.hasUpstream;
- },
- setJob(jobName) {
- this.jobName = jobName;
- },
- setPipelineExpanded(jobName, expanded) {
- if (expanded) {
- this.pipelineExpanded = {
- jobName,
- expanded,
- };
- } else {
- this.pipelineExpanded = {
- expanded,
- jobName: '',
- };
- }
- },
- },
-};
-</script>
-<template>
- <div class="build-content middle-block js-pipeline-graph">
- <div
- class="pipeline-visualization pipeline-graph"
- :class="{ 'pipeline-tab-content': !isLinkedPipeline }"
- >
- <div class="gl-w-full">
- <div class="container-fluid container-limited">
- <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
- <pipeline-graph-legacy
- v-if="pipelineTypeUpstream"
- :type="$options.upstream"
- class="d-inline-block upstream-pipeline"
- :class="`js-upstream-pipeline-${expandedUpstream.id}`"
- :is-loading="false"
- :pipeline="expandedUpstream"
- :is-linked-pipeline="true"
- :mediator="mediator"
- @onClickUpstreamPipeline="clickUpstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
-
- <linked-pipelines-column-legacy
- v-if="hasUpstream"
- :type="$options.upstream"
- :linked-pipelines="upstreamPipelines"
- :column-title="__('Upstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
- />
-
- <ul
- v-if="!isLoading"
- :class="{
- 'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
- }"
- class="stage-column-list align-top"
- >
- <stage-column-component-legacy
- v-for="(stage, index) in graph"
- :key="stage.name"
- :class="{
- 'has-upstream gl-ml-11': hasUpstreamColumn(index),
- 'has-only-one-job': hasOnlyOneJob(stage),
- 'gl-mr-26': shouldAddRightMargin(index),
- }"
- :title="capitalizeStageName(stage.name)"
- :groups="stage.groups"
- :stage-connector-class="stageConnectorClass(index, stage)"
- :is-first-column="isFirstColumn(index)"
- :has-upstream="hasUpstream"
- :action="stage.status.action"
- :job-hovered="jobName"
- :pipeline-expanded="pipelineExpanded"
- @refreshPipelineGraph="refreshPipelineGraph"
- />
- </ul>
-
- <linked-pipelines-column-legacy
- v-if="hasDownstream"
- :type="$options.downstream"
- :linked-pipelines="downstreamPipelines"
- :column-title="__('Downstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="handleClickedDownstream"
- @downstreamHovered="setJob"
- @pipelineExpandToggle="setPipelineExpanded"
- />
-
- <pipeline-graph-legacy
- v-if="pipelineTypeDownstream"
- :type="$options.downstream"
- class="d-inline-block"
- :class="`js-downstream-pipeline-${expandedDownstream.id}`"
- :is-loading="false"
- :pipeline="expandedDownstream"
- :is-linked-pipeline="true"
- :style="{ 'margin-top': downstreamMarginTop }"
- :mediator="mediator"
- @onClickDownstreamPipeline="clickDownstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
- </div>
- </div>
- </div>
- </div>
-</template>
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 a948a57c144..e995d400907 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -4,15 +4,15 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
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 DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
+import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.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';
import GraphViewSelector from './graph_view_selector.vue';
import {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeLoadErrors,
toggleQueryPollingByVisibility,
@@ -31,7 +31,6 @@ export default {
LocalStorageSync,
PipelineGraph,
},
- mixins: [glFeatureFlagMixin()],
inject: {
graphqlResourceEtag: {
default: '',
@@ -50,9 +49,10 @@ export default {
return {
alertType: null,
callouts: [],
+ computedPipelineInfo: null,
currentViewType: STAGE_VIEW,
+ canRefetchHeaderPipeline: false,
pipeline: null,
- pipelineLayers: null,
showAlert: false,
showLinks: false,
};
@@ -78,6 +78,26 @@ export default {
);
},
},
+ headerPipeline: {
+ query: getPipelineQuery,
+ // this query is already being called in header_component.vue, which shares the same cache as this component
+ // the skip here is to prevent sending double network requests on page load
+ skip() {
+ return !this.canRefetchHeaderPipeline;
+ },
+ variables() {
+ return {
+ fullPath: this.pipelineProjectPath,
+ iid: this.pipelineIid,
+ };
+ },
+ update(data) {
+ return data.project?.pipeline || {};
+ },
+ error() {
+ this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
+ },
+ },
pipeline: {
context() {
return getQueryHeaders(this.graphqlResourceEtag);
@@ -178,7 +198,7 @@ export default {
return this.$apollo.queries.pipeline.loading && !this.pipeline;
},
showGraphViewSelector() {
- return Boolean(this.glFeatures.pipelineGraphLayersView && this.pipeline?.usesNeeds);
+ return this.pipeline?.usesNeeds;
},
},
mounted() {
@@ -192,12 +212,16 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
methods: {
- getPipelineLayers() {
- if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) {
- this.pipelineLayers = listByLayers(this.pipeline);
+ getPipelineInfo() {
+ if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) {
+ this.computedPipelineInfo = calculatePipelineLayersInfo(
+ this.pipeline,
+ this.$options.name,
+ this.metricsPath,
+ );
}
- return this.pipelineLayers;
+ return this.computedPipelineInfo;
},
handleTipDismissal() {
try {
@@ -217,6 +241,10 @@ export default {
},
refreshPipelineGraph() {
this.$apollo.queries.pipeline.refetch();
+
+ // this will update the status in header_component since they share the same cache
+ this.canRefetchHeaderPipeline = true;
+ this.$apollo.queries.headerPipeline.refetch();
},
/* eslint-disable @gitlab/require-i18n-strings */
reportFailure({ type, err = 'No error string passed.', skipSentry = false }) {
@@ -262,7 +290,7 @@ export default {
v-if="pipeline"
:config-paths="configPaths"
:pipeline="pipeline"
- :pipeline-layers="getPipelineLayers()"
+ :computed-pipeline-info="getPipelineInfo()"
:show-links="showLinks"
:view-type="graphViewType"
@error="reportFailure"
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 52ee40bd982..d251e0d8bd8 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -2,10 +2,10 @@
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
import LinkedPipeline from './linked_pipeline.vue';
import {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeLoadErrors,
toggleQueryPollingByVisibility,
@@ -138,7 +138,11 @@ export default {
},
getPipelineLayers(id) {
if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
- this.pipelineLayers[id] = listByLayers(this.currentPipeline);
+ this.pipelineLayers[id] = calculatePipelineLayersInfo(
+ this.currentPipeline,
+ this.$options.name,
+ this.configPaths.metricsPath,
+ );
}
return this.pipelineLayers[id];
@@ -223,7 +227,7 @@ export default {
class="d-inline-block gl-mt-n2"
:config-paths="configPaths"
:pipeline="currentPipeline"
- :pipeline-layers="getPipelineLayers(pipeline.id)"
+ :computed-pipeline-info="getPipelineLayers(pipeline.id)"
:show-links="showLinks"
:is-linked-pipeline="true"
:view-type="graphViewType"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
deleted file mode 100644
index 39baeb6e1c3..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import { reportToSentry } from '../../utils';
-import { UPSTREAM } from './constants';
-import LinkedPipeline from './linked_pipeline.vue';
-
-export default {
- components: {
- LinkedPipeline,
- },
- props: {
- columnTitle: {
- type: String,
- required: true,
- },
- linkedPipelines: {
- type: Array,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- },
- computed: {
- columnClass() {
- const positionValues = {
- right: 'gl-ml-11',
- left: 'gl-mr-7',
- };
- return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
- },
- graphPosition() {
- return this.isUpstream ? 'left' : 'right';
- },
- isExpanded() {
- return this.pipeline?.isExpanded || false;
- },
- isUpstream() {
- return this.type === UPSTREAM;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('linked_pipelines_column_legacy', `error: ${err}, info: ${info}`);
- },
- methods: {
- onPipelineClick(downstreamNode, pipeline, index) {
- this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
- },
- onDownstreamHovered(jobName) {
- this.$emit('downstreamHovered', jobName);
- },
- onPipelineExpandToggle(jobName, expanded) {
- // Highlighting only applies to downstream pipelines
- if (this.isUpstream) {
- return;
- }
-
- this.$emit('pipelineExpandToggle', jobName, expanded);
- },
- },
-};
-</script>
-
-<template>
- <div :class="columnClass" class="stage-column linked-pipelines-column">
- <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
- <div v-if="isUpstream" class="cross-project-triangle"></div>
- <ul>
- <li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
- <linked-pipeline
- :class="{
- active: pipeline.isExpanded,
- 'left-connector': pipeline.isExpanded && graphPosition === 'left',
- }"
- :pipeline="pipeline"
- :column-title="columnTitle"
- :project-id="projectId"
- :type="type"
- :expanded="isExpanded"
- @pipelineClicked="onPipelineClick($event, pipeline, index)"
- @downstreamHovered="onDownstreamHovered"
- @pipelineExpandToggle="onPipelineExpandToggle"
- />
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/pipelines/components/graph/perf_utils.js
new file mode 100644
index 00000000000..3737a209f5c
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/perf_utils.js
@@ -0,0 +1,50 @@
+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 { reportPerformance } from '../graph_shared/api';
+
+export const beginPerfMeasure = () => {
+ performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
+};
+
+export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => {
+ 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: numLinks },
+ {
+ name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
+ value: numLinks / numGroups,
+ },
+ ],
+ };
+
+ reportPerformance(metricsPath, data);
+ });
+};
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
deleted file mode 100644
index cbaf07c05cf..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
-import { isEmpty, escape } from 'lodash';
-import stageColumnMixin from '../../mixins/stage_column_mixin';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobGroupDropdown from './job_group_dropdown.vue';
-import JobItem from './job_item.vue';
-
-export default {
- components: {
- JobItem,
- JobGroupDropdown,
- ActionComponent,
- },
- mixins: [stageColumnMixin],
- props: {
- title: {
- type: String,
- required: true,
- },
- groups: {
- type: Array,
- required: true,
- },
- isFirstColumn: {
- type: Boolean,
- required: false,
- default: false,
- },
- stageConnectorClass: {
- type: String,
- required: false,
- default: '',
- },
- action: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- jobHovered: {
- type: String,
- required: false,
- default: '',
- },
- pipelineExpanded: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- },
- computed: {
- hasAction() {
- return !isEmpty(this.action);
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('stage_column_component_legacy', `error: ${err}, info: ${info}`);
- },
- methods: {
- groupId(group) {
- return `ci-badge-${escape(group.name)}`;
- },
- pipelineActionRequestComplete() {
- this.$emit('refreshPipelineGraph');
- },
- },
-};
-</script>
-<template>
- <li :class="stageConnectorClass" class="stage-column">
- <div class="stage-name position-relative" data-testid="stage-column-title">
- {{ title }}
- <action-component
- v-if="hasAction"
- :action-icon="action.icon"
- :tooltip-text="action.title"
- :link="action.path"
- class="js-stage-action stage-action rounded"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </div>
-
- <div class="builds-container">
- <ul>
- <li
- v-for="(group, index) in groups"
- :id="groupId(group)"
- :key="group.id"
- :class="buildConnnectorClass(index)"
- class="build"
- >
- <div class="curve"></div>
-
- <job-item
- v-if="group.size === 1"
- :job="group.jobs[0]"
- :job-hovered="jobHovered"
- :pipeline-expanded="pipelineExpanded"
- css-class-job-name="build-content"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
-
- <job-group-dropdown
- v-if="group.size > 1"
- :group="group"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index 163b3898c28..3da792cb9df 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -1,7 +1,10 @@
import { isEmpty } from 'lodash';
import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { reportToSentry } from '../../utils';
+import { listByLayers } from '../parsing_utils';
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
+import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return {
@@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
};
};
+const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
+ const shouldCollectMetrics = Boolean(metricsPath);
+
+ if (shouldCollectMetrics) {
+ beginPerfMeasure();
+ }
+
+ let layers = null;
+
+ try {
+ layers = listByLayers(pipeline);
+
+ if (shouldCollectMetrics) {
+ finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath);
+ }
+ } catch (err) {
+ reportToSentry(componentName, err);
+ }
+
+ return layers;
+};
+
/* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => {
return {
@@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
export {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeGqlErr,
serializeLoadErrors,
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 83f2466f0bf..d6d9ea94c13 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, containerID, modifier = '') => {
+export const generateLinksData = (links, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID);
return links.map((link) => {
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 5c775df7b48..1189c2ebad8 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -17,8 +17,8 @@ export default {
type: Object,
required: true,
},
- parsedData: {
- type: Object,
+ linksData: {
+ type: Array,
required: true,
},
pipelineId: {
@@ -95,7 +95,7 @@ export default {
highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs);
},
- parsedData() {
+ linksData() {
this.calculateLinkData();
},
viewType() {
@@ -112,7 +112,7 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- if (!isEmpty(this.parsedData)) {
+ if (!isEmpty(this.linksData)) {
this.calculateLinkData();
}
},
@@ -122,7 +122,7 @@ export default {
},
calculateLinkData() {
try {
- this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
+ this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`);
} catch (err) {
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
reportToSentry(this.$options.name, err);
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 81409752621..ef24694e494 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -1,20 +1,16 @@
<script>
-import { isEmpty } from 'lodash';
-import { __ } from '~/locale';
-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 { memoize } from 'lodash';
import { reportToSentry } from '../../utils';
import { parseData } from '../parsing_utils';
-import { reportPerformance } from './api';
import LinksInner from './links_inner.vue';
+const parseForLinksBare = (pipeline) => {
+ const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
+ return parseData(arrayOfJobs).links;
+};
+
+const parseForLinks = memoize(parseForLinksBare);
+
export default {
name: 'LinksLayer',
components: {
@@ -29,10 +25,10 @@ export default {
type: Array,
required: true,
},
- metricsConfig: {
- type: Object,
+ linksData: {
+ type: Array,
required: false,
- default: () => ({}),
+ default: () => [],
},
showLinks: {
type: Boolean,
@@ -40,30 +36,16 @@ export default {
default: true,
},
},
- data() {
- return {
- alertDismissed: false,
- parsedData: {},
- showLinksOverride: false,
- };
- },
- i18n: {
- showLinksAnyways: __('Show links anyways'),
- tooManyJobs: __(
- 'This graph has a large number of jobs and showing the links between them may have performance implications.',
- ),
- },
computed: {
containerZero() {
return !this.containerMeasurements.width || !this.containerMeasurements.height;
},
- numGroups() {
- return this.pipelineData.reduce((acc, { groups }) => {
- return acc + Number(groups.length);
- }, 0);
- },
- shouldCollectMetrics() {
- return this.metricsConfig.collectMetrics && this.metricsConfig.path;
+ getLinksData() {
+ if (this.linksData.length > 0) {
+ return this.linksData;
+ }
+
+ return parseForLinks(this.pipelineData);
},
showLinkedLayers() {
return this.showLinks && !this.containerZero;
@@ -72,77 +54,14 @@ export default {
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
- mounted() {
- if (!isEmpty(this.pipelineData)) {
- window.requestAnimationFrame(() => {
- this.prepareLinkData();
- });
- }
- },
- methods: {
- beginPerfMeasure() {
- if (this.shouldCollectMetrics) {
- performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
- }
- },
- finishPerfMeasureAndSend(numLinks) {
- 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: numLinks },
- {
- name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
- value: numLinks / this.numGroups,
- },
- ],
- };
-
- reportPerformance(this.metricsConfig.path, data);
- });
- },
- prepareLinkData() {
- this.beginPerfMeasure();
- let numLinks;
- try {
- const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
- this.parsedData = parseData(arrayOfJobs);
- numLinks = this.parsedData.links.length;
- } catch (err) {
- reportToSentry(this.$options.name, err);
- }
- this.finishPerfMeasureAndSend(numLinks);
- },
- },
};
</script>
<template>
<links-inner
v-if="showLinkedLayers"
:container-measurements="containerMeasurements"
- :parsed-data="parsedData"
+ :links-data="getLinksData"
:pipeline-data="pipelineData"
- :total-groups="numGroups"
v-bind="$attrs"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index b7500ef00b0..5db2b604956 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -143,13 +143,6 @@ export default {
return cancelable && userPermissions.updatePipeline;
},
},
- watch: {
- isFinished(finished) {
- if (finished) {
- this.$apollo.queries.pipeline.stopPolling();
- }
- },
- },
methods: {
reportFailure(errorType) {
this.failureType = errorType;
@@ -218,7 +211,7 @@ export default {
};
</script>
<template>
- <div class="pipeline-header-container">
+ <div class="js-pipeline-header-container">
<gl-alert v-if="hasError" :variant="failure.variant">{{ failure.text }}</gl-alert>
<ci-header
v-if="shouldRenderContent"
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index b36c9c0d049..7e7f0572faf 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -175,7 +175,7 @@ export const listByLayers = ({ stages }) => {
const parsedData = parseData(arrayOfJobs);
const dataWithLayers = createSankey()(parsedData);
- return dataWithLayers.nodes.reduce((acc, { layer, name }) => {
+ const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */
if (!acc[layer]) {
@@ -186,6 +186,12 @@ export const listByLayers = ({ stages }) => {
return acc;
}, []);
+
+ return {
+ linksData: parsedData.links,
+ numGroups: arrayOfJobs.length,
+ pipelineLayers,
+ };
};
export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index 5e18f636b52..40ee071f1f5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -16,6 +16,7 @@ export const i18n = {
downloadArtifact: __('Download %{name} artifact'),
artifactSectionHeader: __('Download artifacts'),
artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
+ emptyArtifactsMessage: __('No artifacts found'),
};
export default {
@@ -99,6 +100,10 @@ export default {
<gl-loading-icon v-if="isLoading" size="sm" />
+ <gl-dropdown-item v-if="!artifacts.length" data-testid="artifacts-empty-message">
+ {{ $options.i18n.emptyArtifactsMessage }}
+ </gl-dropdown-item>
+
<gl-dropdown-item
v-for="(artifact, i) in artifacts"
:key="i"
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 85ee44f427d..b6c178d20b0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
@@ -95,10 +95,10 @@ export default {
:title="$options.i18n.cancelTitle"
:loading="isCancelling"
:disabled="isCancelling"
- icon="close"
+ icon="cancel"
variant="danger"
category="primary"
- class="js-pipelines-cancel-button"
+ class="js-pipelines-cancel-button gl-ml-1"
@click="handleCancelClick"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index fc8f31c5b7e..e2f30d5a8e6 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
+ pipelineKey: {
+ type: String,
+ required: true,
+ },
},
computed: {
user() {
@@ -60,7 +64,7 @@ export default {
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
- #{{ pipeline.id }}
+ #{{ pipeline[pipelineKey] }}
</gl-link>
<div class="label-container">
<gl-badge
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index e3373178239..e7ff5449331 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,12 +1,17 @@
<script>
-import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import createFlash from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
+import {
+ ANY_TRIGGER_AUTHOR,
+ RAW_TEXT_WARNING,
+ FILTER_TAG_IDENTIFIER,
+ PipelineKeyOptions,
+} from '../../constants';
import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
@@ -16,8 +21,11 @@ import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue';
export default {
+ PipelineKeyOptions,
components: {
EmptyState,
+ GlDropdown,
+ GlDropdownItem,
GlEmptyState,
GlIcon,
GlLoadingIcon,
@@ -114,6 +122,7 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
isResetCacheButtonLoading: false,
+ selectedPipelineKeyOption: this.$options.PipelineKeyOptions[0],
};
},
stateMap: {
@@ -301,6 +310,9 @@ export default {
this.updateContent(this.requestData);
},
+ changeVisibilityPipelineID(val) {
+ this.selectedPipelineKeyOption = val;
+ },
},
};
</script>
@@ -330,12 +342,31 @@ export default {
/>
</div>
- <pipelines-filtered-search
- v-if="stateToRender !== $options.stateMap.emptyState"
- :project-id="projectId"
- :params="validatedParams"
- @filterPipelines="filterPipelines"
- />
+ <div v-if="stateToRender !== $options.stateMap.emptyState" class="gl-display-flex">
+ <div class="row-content-block gl-display-flex gl-flex-grow-1">
+ <pipelines-filtered-search
+ class="gl-display-flex gl-flex-grow-1 gl-mr-4"
+ :project-id="projectId"
+ :params="validatedParams"
+ @filterPipelines="filterPipelines"
+ />
+ <gl-dropdown
+ class="gl-display-flex"
+ :text="selectedPipelineKeyOption.text"
+ data-testid="pipeline-key-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="(val, index) in $options.PipelineKeyOptions"
+ :key="index"
+ :is-checked="selectedPipelineKeyOption.key === val.key"
+ is-check-item
+ @click="changeVisibilityPipelineID(val)"
+ >
+ {{ val.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
<div class="content-list pipelines">
<gl-loading-icon
@@ -374,6 +405,7 @@ export default {
:pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
+ :pipeline-key-option="selectedPipelineKeyOption"
/>
</div>
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 de3f783ac84..0b70e74b8ff 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
@@ -4,6 +4,7 @@ import { map } from 'lodash';
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 PipelineSourceToken from './tokens/pipeline_source_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
@@ -13,6 +14,7 @@ export default {
branchType: 'ref',
tagType: 'tag',
statusType: 'status',
+ sourceType: 'source',
defaultTokensLength: 1,
components: {
GlFilteredSearch,
@@ -37,7 +39,7 @@ export default {
return this.value.map((i) => i.type);
},
tokens() {
- return [
+ const tokens = [
{
type: this.$options.userType,
icon: 'user',
@@ -76,6 +78,19 @@ export default {
operators: OPERATOR_IS_ONLY,
},
];
+
+ if (gon.features.pipelineSourceFilter) {
+ tokens.push({
+ type: this.$options.sourceType,
+ icon: 'trigger-source',
+ title: s__('Pipeline|Source'),
+ unique: true,
+ token: PipelineSourceToken,
+ operators: OPERATOR_IS_ONLY,
+ });
+ }
+
+ return tokens;
},
parsedParams() {
return map(this.params, (val, key) => ({
@@ -101,12 +116,10 @@ export default {
</script>
<template>
- <div class="row-content-block">
- <gl-filtered-search
- v-model="value"
- :placeholder="__('Filter pipelines')"
- :available-tokens="tokens"
- @submit="onSubmit"
- />
- </div>
+ <gl-filtered-search
+ v-model="value"
+ :placeholder="__('Filter pipelines')"
+ :available-tokens="tokens"
+ @submit="onSubmit"
+ />
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 47fc7023222..2475d958e3c 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -17,65 +17,10 @@ const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!';
export default {
- fields: [
- {
- key: 'status',
- label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
- columnClass: 'gl-w-10p',
- tdClass: DEFAULT_TD_CLASS,
- thAttr: { 'data-testid': 'status-th' },
- },
- {
- key: 'pipeline',
- label: s__('Pipeline|Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
- columnClass: 'gl-w-10p',
- thAttr: { 'data-testid': 'pipeline-th' },
- },
- {
- key: 'triggerer',
- label: s__('Pipeline|Triggerer'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
- columnClass: 'gl-w-10p',
- thAttr: { 'data-testid': 'triggerer-th' },
- },
- {
- key: 'commit',
- label: s__('Pipeline|Commit'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: DEFAULT_TD_CLASS,
- columnClass: 'gl-w-20p',
- thAttr: { 'data-testid': 'commit-th' },
- },
- {
- key: 'stages',
- label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: DEFAULT_TD_CLASS,
- columnClass: 'gl-w-15p',
- thAttr: { 'data-testid': 'stages-th' },
- },
- {
- key: 'timeago',
- label: s__('Pipeline|Duration'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: DEFAULT_TD_CLASS,
- columnClass: 'gl-w-15p',
- thAttr: { 'data-testid': 'timeago-th' },
- },
- {
- key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
- tdClass: DEFAULT_TD_CLASS,
- columnClass: 'gl-w-20p',
- thAttr: { 'data-testid': 'actions-th' },
- },
- ],
components: {
GlTable,
+ LinkedPipelinesMiniList: () =>
+ import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
PipelineMiniGraph,
PipelineOperations,
@@ -107,6 +52,10 @@ export default {
type: String,
required: true,
},
+ pipelineKeyOption: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -116,6 +65,68 @@ export default {
cancelingPipeline: null,
};
},
+ computed: {
+ tableFields() {
+ const fields = [
+ {
+ key: 'status',
+ label: s__('Pipeline|Status'),
+ thClass: DEFAULT_TH_CLASSES,
+ columnClass: 'gl-w-10p',
+ tdClass: DEFAULT_TD_CLASS,
+ thAttr: { 'data-testid': 'status-th' },
+ },
+ {
+ key: 'pipeline',
+ label: this.pipelineKeyOption.label,
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
+ columnClass: 'gl-w-10p',
+ thAttr: { 'data-testid': 'pipeline-th' },
+ },
+ {
+ key: 'triggerer',
+ label: s__('Pipeline|Triggerer'),
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
+ columnClass: 'gl-w-10p',
+ thAttr: { 'data-testid': 'triggerer-th' },
+ },
+ {
+ key: 'commit',
+ label: s__('Pipeline|Commit'),
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: DEFAULT_TD_CLASS,
+ columnClass: 'gl-w-20p',
+ thAttr: { 'data-testid': 'commit-th' },
+ },
+ {
+ key: 'stages',
+ label: s__('Pipeline|Stages'),
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: DEFAULT_TD_CLASS,
+ columnClass: 'gl-w-quarter',
+ thAttr: { 'data-testid': 'stages-th' },
+ },
+ {
+ key: 'timeago',
+ label: s__('Pipeline|Duration'),
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: DEFAULT_TD_CLASS,
+ columnClass: 'gl-w-15p',
+ thAttr: { 'data-testid': 'timeago-th' },
+ },
+ {
+ key: 'actions',
+ thClass: DEFAULT_TH_CLASSES,
+ tdClass: DEFAULT_TD_CLASS,
+ columnClass: 'gl-w-15p',
+ thAttr: { 'data-testid': 'actions-th' },
+ },
+ ];
+ return fields;
+ },
+ },
watch: {
pipelines() {
this.cancelingPipeline = null;
@@ -146,7 +157,7 @@ export default {
<template>
<div class="ci-table">
<gl-table
- :fields="$options.fields"
+ :fields="tableFields"
:items="pipelines"
tbody-tr-class="commit"
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
@@ -167,7 +178,11 @@ export default {
</template>
<template #cell(pipeline)="{ item }">
- <pipeline-url :pipeline="item" :pipeline-schedule-url="pipelineScheduleUrl" />
+ <pipeline-url
+ :pipeline="item"
+ :pipeline-schedule-url="pipelineScheduleUrl"
+ :pipeline-key="pipelineKeyOption.key"
+ />
</template>
<template #cell(triggerer)="{ item }">
@@ -182,12 +197,23 @@ export default {
<div class="stage-cell">
<!-- This empty div should be removed, see https://gitlab.com/gitlab-org/gitlab/-/issues/323488 -->
<div></div>
+ <linked-pipelines-mini-list
+ v-if="item.triggered_by"
+ :triggered-by="[item.triggered_by]"
+ data-testid="mini-graph-upstream"
+ />
<pipeline-mini-graph
v-if="item.details && item.details.stages && item.details.stages.length > 0"
+ class="gl-display-inline"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
/>
+ <linked-pipelines-mini-list
+ v-if="item.triggered.length"
+ :triggered="item.triggered"
+ data-testid="mini-graph-downstream"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
new file mode 100644
index 00000000000..71efa8b2ab4
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ sources() {
+ return [
+ {
+ text: s__('Pipeline|Source|Push'),
+ value: 'push',
+ },
+ {
+ text: s__('Pipeline|Source|Web'),
+ value: 'web',
+ },
+ {
+ text: s__('Pipeline|Source|Trigger'),
+ value: 'trigger',
+ },
+ {
+ text: s__('Pipeline|Source|Schedule'),
+ value: 'schedule',
+ },
+ {
+ text: s__('Pipeline|Source|API'),
+ value: 'api',
+ },
+ {
+ text: s__('Pipeline|Source|External'),
+ value: 'external',
+ },
+ {
+ text: s__('Pipeline|Source|Pipeline'),
+ value: 'pipeline',
+ },
+ {
+ text: s__('Pipeline|Source|Chat'),
+ value: 'chat',
+ },
+ {
+ text: s__('Pipeline|Source|Web IDE'),
+ value: 'webide',
+ },
+ {
+ text: s__('Pipeline|Source|Merge Request'),
+ value: 'merge_request_event',
+ },
+ {
+ text: s__('Pipeline|Source|External Pull Request'),
+ value: 'external_pull_request_event',
+ },
+ {
+ text: s__('Pipeline|Source|Parent Pipeline'),
+ value: 'parent_pipeline',
+ },
+ {
+ text: s__('Pipeline|Source|On-Demand DAST Scan'),
+ value: 'ondemand_dast_scan',
+ },
+ {
+ text: s__('Pipeline|Source|On-Demand DAST Validation'),
+ value: 'ondemand_dast_validation',
+ },
+ ];
+ },
+ findActiveSource() {
+ return this.sources.find((source) => source.value === this.value.data);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
+ <template #view>
+ <div class="gl-display-flex gl-align-items-center">
+ <span>{{ findActiveSource.text }}</span>
+ </div>
+ </template>
+
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="source in sources"
+ :key="source.value"
+ :value="source.value"
+ >
+ {{ source.text }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 21b114825a6..5678b613ec6 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -4,7 +4,7 @@ export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const LAYOUT_CHANGE_DELAY = 300;
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
export const ANY_TRIGGER_AUTHOR = 'Any';
-export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status'];
+export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
@@ -35,3 +35,17 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
+
+// Constants for the ID and IID selection dropdown
+export const PipelineKeyOptions = [
+ {
+ text: __('Show Pipeline ID'),
+ label: __('Pipeline ID'),
+ key: 'id',
+ },
+ {
+ text: __('Show Pipeline IID'),
+ label: __('Pipeline IID'),
+ key: 'iid',
+ },
+];
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
deleted file mode 100644
index 5c34f4e4f7e..00000000000
--- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-
-export default {
- methods: {
- getExpandedPipelines(pipeline) {
- this.mediator.service
- .getPipeline(this.mediator.getExpandedParameters())
- .then((response) => {
- this.mediator.store.toggleLoading(pipeline);
- this.mediator.store.storePipeline(response.data);
- this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
- })
- .catch(() => {
- this.mediator.store.toggleLoading(pipeline);
- createFlash({
- message: __('An error occurred while fetching the pipeline.'),
- });
- });
- },
- /**
- * Called when a linked pipeline is clicked.
- *
- * If the pipeline is collapsed we will start polling it & we will reset the other pipelines.
- * If the pipeline is expanded we will close it.
- *
- * @param {String} method Method to fetch the pipeline
- * @param {String} storeKey Store property that will be updates
- * @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset
- * @param {Object} pipeline The clicked pipeline
- */
- clickPipeline(pipeline, openMethod, closeMethod) {
- if (!pipeline.isExpanded) {
- this.mediator.store[openMethod](pipeline);
- this.mediator.store.toggleLoading(pipeline);
- this.mediator.poll.stop();
-
- this.getExpandedPipelines(pipeline);
- } else {
- this.mediator.store[closeMethod](pipeline);
- this.mediator.poll.stop();
-
- this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() });
- }
- },
- resetDownstreamPipelines(parentPipeline, pipeline) {
- this.mediator.store.resetTriggeredPipelines(parentPipeline, pipeline);
- },
- clickUpstreamPipeline(pipeline) {
- this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
- },
- clickDownstreamPipeline(pipeline) {
- this.clickPipeline(pipeline, 'openPipeline', 'closePipeline');
- },
- requestRefreshPipelineGraph() {
- // When an action is clicked
- // (whether in the dropdown or in the main nodes, we refresh the big graph)
- this.mediator.refreshPipeline().catch(() =>
- createFlash({
- message: __('An error occurred while making the request.'),
- }),
- );
- },
- },
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index e8d5ed175ba..c6e767d5424 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -3,15 +3,12 @@ import createFlash 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';
import TestReports from './components/test_reports/test_reports.vue';
-import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports';
-import { reportToSentry } from './utils';
Vue.use(Translate);
@@ -22,44 +19,6 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
-const createLegacyPipelinesDetailApp = (mediator) => {
- if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) {
- return;
- }
- // eslint-disable-next-line no-new
- new Vue({
- el: SELECTORS.PIPELINE_GRAPH,
- components: {
- PipelineGraphLegacy,
- },
- mixins: [GraphBundleMixin],
- data() {
- return {
- mediator,
- };
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipeline_details_bundle_legacy_details', `error: ${err}, info: ${info}`);
- },
- render(createElement) {
- return createElement('pipeline-graph-legacy', {
- props: {
- isLoading: this.mediator.state.isLoading,
- pipeline: this.mediator.store.state.pipeline,
- mediator: this.mediator,
- },
- on: {
- refreshPipelineGraph: this.requestRefreshPipelineGraph,
- onResetDownstream: (parentPipeline, pipeline) =>
- this.resetDownstreamPipelines(parentPipeline, pipeline),
- onClickUpstreamPipeline: (pipeline) => this.clickUpstreamPipeline(pipeline),
- onClickDownstreamPipeline: (pipeline) => this.clickDownstreamPipeline(pipeline),
- },
- });
- },
- });
-};
-
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
@@ -88,9 +47,6 @@ const createTestDetails = () => {
};
export default async function initPipelineDetailsBundle() {
- const canShowNewPipelineDetails =
- gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
-
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
try {
@@ -101,22 +57,12 @@ export default async function initPipelineDetailsBundle() {
});
}
- if (canShowNewPipelineDetails) {
- try {
- createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
- } catch {
- createFlash({
- message: __('An error occurred while loading the pipeline.'),
- });
- }
- } else {
- const { default: PipelinesMediator } = await import(
- /* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
- );
- const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
- mediator.fetchPipeline();
-
- createLegacyPipelinesDetailApp(mediator);
+ try {
+ createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
+ } catch {
+ createFlash({
+ message: __('An error occurred while loading the pipeline.'),
+ });
}
createDagApp(apolloProvider);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
deleted file mode 100644
index 72c4fedc64c..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
-import Poll from '../lib/utils/poll';
-import { __ } from '../locale';
-import PipelineService from './services/pipeline_service';
-import PipelineStore from './stores/pipeline_store';
-
-export default class pipelinesMediator {
- constructor(options = {}) {
- this.options = options;
- this.store = new PipelineStore();
- this.service = new PipelineService(options.endpoint);
-
- this.state = {};
- this.state.isLoading = false;
- }
-
- fetchPipeline() {
- this.poll = new Poll({
- resource: this.service,
- method: 'getPipeline',
- data: this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
- successCallback: this.successCallback.bind(this),
- errorCallback: this.errorCallback.bind(this),
- });
-
- if (!Visibility.hidden()) {
- this.state.isLoading = true;
- this.poll.makeRequest();
- } else {
- this.refreshPipeline();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- this.poll.restart();
- } else {
- this.stopPipelinePoll();
- }
- });
- }
-
- successCallback(response) {
- this.state.isLoading = false;
- this.store.storePipeline(response.data);
- }
-
- errorCallback() {
- this.state.isLoading = false;
- createFlash({
- message: __('An error occurred while fetching the pipeline.'),
- });
- }
-
- refreshPipeline() {
- this.stopPipelinePoll();
-
- return this.service
- .getPipeline()
- .then((response) => this.successCallback(response))
- .catch(() => this.errorCallback())
- .finally(() =>
- this.poll.restart(
- this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
- ),
- );
- }
-
- stopPipelinePoll() {
- this.poll.stop();
- }
-
- /**
- * Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
- */
- getExpandedParameters() {
- return {
- expanded: this.store.state.expandedPipelines,
- };
- }
-}
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
deleted file mode 100644
index ba2830ec596..00000000000
--- a/app/assets/javascripts/pipelines/services/pipeline_service.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import axios from '../../lib/utils/axios_utils';
-
-export default class PipelineService {
- constructor(endpoint) {
- this.pipeline = endpoint;
- }
-
- getPipeline(params) {
- return axios.get(this.pipeline, { params });
- }
-
- // eslint-disable-next-line class-methods-use-this
- deleteAction(endpoint) {
- return axios.delete(`${endpoint}.json`);
- }
-
- // eslint-disable-next-line class-methods-use-this
- postAction(endpoint) {
- return axios.post(`${endpoint}.json`);
- }
-}
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
deleted file mode 100644
index 1f804a107a8..00000000000
--- a/app/assets/javascripts/pipelines/stores/pipeline_store.js
+++ /dev/null
@@ -1,206 +0,0 @@
-import Vue from 'vue';
-
-export default class PipelineStore {
- constructor() {
- this.state = {};
- this.state.pipeline = {};
- this.state.expandedPipelines = [];
- }
- /**
- * For the triggered pipelines adds the `isExpanded` key
- *
- * For the triggered_by pipeline adds the `isExpanded` key
- * and saves it as an array
- *
- * @param {Object} pipeline
- */
- storePipeline(pipeline = {}) {
- const pipelineCopy = { ...pipeline };
-
- if (pipelineCopy.triggered_by) {
- pipelineCopy.triggered_by = [pipelineCopy.triggered_by];
-
- const oldTriggeredBy =
- this.state.pipeline &&
- this.state.pipeline.triggered_by &&
- this.state.pipeline.triggered_by[0];
-
- this.parseTriggeredByPipelines(oldTriggeredBy, pipelineCopy.triggered_by[0]);
- }
-
- if (pipelineCopy.triggered && pipelineCopy.triggered.length) {
- pipelineCopy.triggered.forEach((el) => {
- const oldPipeline =
- this.state.pipeline &&
- this.state.pipeline.triggered &&
- this.state.pipeline.triggered.find((element) => element.id === el.id);
-
- this.parseTriggeredPipelines(oldPipeline, el);
- });
- }
-
- this.state.pipeline = pipelineCopy;
- }
-
- /**
- * Recursiverly parses the triggered by pipelines.
- *
- * Sets triggered_by as an array, there is always only 1 triggered_by pipeline.
- * Adds key `isExpanding`
- * Keeps old isExpading value due to polling
- *
- * @param {Array} parentPipeline
- * @param {Object} pipeline
- */
- parseTriggeredByPipelines(oldPipeline = {}, newPipeline) {
- // keep old value in case it's opened because we're polling
- Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
- // add isLoading property
- Vue.set(newPipeline, 'isLoading', false);
-
- // Because there can only ever be one `triggered_by` for any given pipeline,
- // the API returns an object for the value instead of an Array. However,
- // it's easier to deal with an array in the FE so we convert it.
- if (newPipeline.triggered_by) {
- if (!Array.isArray(newPipeline.triggered_by)) {
- Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] });
- }
-
- if (newPipeline.triggered_by?.length > 0) {
- newPipeline.triggered_by.forEach((el) => {
- const oldTriggeredBy = oldPipeline.triggered_by?.find((element) => element.id === el.id);
- this.parseTriggeredPipelines(oldTriggeredBy, el);
- });
- }
- }
- }
-
- /**
- * Recursively parses the triggered pipelines
- * @param {Array} parentPipeline
- * @param {Object} pipeline
- */
- parseTriggeredPipelines(oldPipeline = {}, newPipeline) {
- // keep old value in case it's opened because we're polling
- Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false);
-
- // add isLoading property
- Vue.set(newPipeline, 'isLoading', false);
-
- if (newPipeline.triggered && newPipeline.triggered.length > 0) {
- newPipeline.triggered.forEach((el) => {
- const oldTriggered =
- oldPipeline.triggered && oldPipeline.triggered.find((element) => element.id === el.id);
- this.parseTriggeredPipelines(oldTriggered, el);
- });
- }
- }
-
- /**
- * Recursively resets all triggered by pipelines
- *
- * @param {Object} pipeline
- */
- resetTriggeredByPipeline(parentPipeline, pipeline) {
- parentPipeline.triggered_by.forEach((el) => this.closePipeline(el));
-
- if (pipeline.triggered_by && pipeline.triggered_by) {
- this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by);
- }
- }
-
- /**
- * Opens the clicked pipeline and closes all other ones.
- * @param {Object} pipeline
- */
- openTriggeredByPipeline(parentPipeline, pipeline) {
- // first we need to reset all triggeredBy pipelines
- this.resetTriggeredByPipeline(parentPipeline, pipeline);
-
- this.openPipeline(pipeline);
- }
-
- /**
- * On click, will close the given pipeline and all nested triggered by pipelines
- *
- * @param {Object} pipeline
- */
- closeTriggeredByPipeline(pipeline) {
- this.closePipeline(pipeline);
-
- if (pipeline.triggered_by && pipeline.triggered_by.length) {
- pipeline.triggered_by.forEach((triggeredBy) => this.closeTriggeredByPipeline(triggeredBy));
- }
- }
-
- /**
- * Recursively closes all triggered pipelines for the given one.
- *
- * @param {Object} pipeline
- */
- resetTriggeredPipelines(parentPipeline, pipeline) {
- parentPipeline.triggered.forEach((el) => this.closePipeline(el));
-
- if (pipeline.triggered && pipeline.triggered.length) {
- pipeline.triggered.forEach((el) => this.resetTriggeredPipelines(pipeline, el));
- }
- }
-
- /**
- * Opens the clicked triggered pipeline and closes all other ones.
- *
- * @param {Object} pipeline
- */
- openTriggeredPipeline(parentPipeline, pipeline) {
- this.resetTriggeredPipelines(parentPipeline, pipeline);
-
- this.openPipeline(pipeline);
- }
-
- /**
- * On click, will close the given pipeline and all the nested triggered ones
- * @param {Object} pipeline
- */
- closeTriggeredPipeline(pipeline) {
- this.closePipeline(pipeline);
-
- if (pipeline.triggered && pipeline.triggered.length) {
- pipeline.triggered.forEach((triggered) => this.closeTriggeredPipeline(triggered));
- }
- }
-
- /**
- * Utility function, Closes the given pipeline
- * @param {Object} pipeline
- */
- closePipeline(pipeline) {
- Vue.set(pipeline, 'isExpanded', false);
- // remove the pipeline from the parameters
- this.removeExpandedPipelineToRequestData(pipeline.id);
- }
-
- /**
- * Utility function, Opens the given pipeline
- * @param {Object} pipeline
- */
- openPipeline(pipeline) {
- Vue.set(pipeline, 'isExpanded', true);
- // add the pipeline to the parameters
- this.addExpandedPipelineToRequestData(pipeline.id);
- }
- // eslint-disable-next-line class-methods-use-this
- toggleLoading(pipeline) {
- Vue.set(pipeline, 'isLoading', !pipeline.isLoading);
- }
-
- addExpandedPipelineToRequestData(id) {
- this.state.expandedPipelines.push(id);
- }
-
- removeExpandedPipelineToRequestData(id) {
- this.state.expandedPipelines.splice(
- this.state.expandedPipelines.findIndex((el) => el === id),
- 1,
- );
- }
-}
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index a0129dd536b..757a66ef148 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -127,20 +127,18 @@ export default {
:config="$options.integrationViewConfigs[view.name]"
/>
</div>
- <div class="col-lg-4 profile-settings-sidebar"></div>
- <div class="col-lg-8">
- <div class="form-group">
- <gl-button
- category="primary"
- variant="confirm"
- name="commit"
- type="submit"
- :disabled="!isSubmitEnabled"
- :value="$options.i18n.saveChanges"
- >
- {{ $options.i18n.saveChanges }}
- </gl-button>
- </div>
+ <div class="col-sm-12">
+ <hr />
+ <gl-button
+ category="primary"
+ variant="confirm"
+ name="commit"
+ type="submit"
+ :disabled="!isSubmitEnabled"
+ :value="$options.i18n.saveChanges"
+ >
+ {{ $options.i18n.saveChanges }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
new file mode 100644
index 00000000000..a4a1cb5584d
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql';
+
+export default {
+ i18n: {
+ linkedPipelinesFetchError: __('There was a problem fetching linked pipelines.'),
+ },
+ components: {
+ GlLoadingIcon,
+ PipelineMiniGraph,
+ LinkedPipelinesMiniList: () =>
+ import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ iid: {
+ default: '',
+ },
+ },
+ props: {
+ stages: {
+ type: Array,
+ required: true,
+ },
+ },
+ apollo: {
+ pipeline: {
+ query: getLinkedPipelinesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ };
+ },
+ skip() {
+ return !this.fullPath || !this.iid;
+ },
+ update({ project }) {
+ return project?.pipeline;
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.linkedPipelinesFetchError });
+ },
+ },
+ },
+ data() {
+ return {
+ pipeline: null,
+ };
+ },
+ computed: {
+ hasDownstream() {
+ return this.pipeline?.downstream?.nodes.length > 0;
+ },
+ downstreamPipelines() {
+ return this.pipeline?.downstream?.nodes;
+ },
+ upstreamPipeline() {
+ return this.pipeline?.upstream;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
+ <div v-else>
+ <linked-pipelines-mini-list
+ v-if="upstreamPipeline"
+ :triggered-by="[upstreamPipeline]"
+ data-testid="commit-box-mini-graph-upstream"
+ />
+
+ <pipeline-mini-graph
+ :stages="stages"
+ class="gl-display-inline"
+ data-testid="commit-box-mini-graph"
+ />
+
+ <linked-pipelines-mini-list
+ v-if="hasDownstream"
+ :triggered="downstreamPipelines"
+ data-testid="commit-box-mini-graph-downstream"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
new file mode 100644
index 00000000000..f7e930bb3f2
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
@@ -0,0 +1,32 @@
+query getLinkedPipelines($fullPath: ID!, $iid: ID!) {
+ project(fullPath: $fullPath) {
+ pipeline(iid: $iid) {
+ downstream {
+ nodes {
+ id
+ path
+ project {
+ name
+ }
+ detailedStatus {
+ group
+ icon
+ label
+ }
+ }
+ }
+ upstream {
+ id
+ path
+ project {
+ name
+ }
+ detailedStatus {
+ group
+ icon
+ label
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index 9173f5c771f..1d4ec4c110b 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -1,24 +1,41 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
const el = document.querySelector(selector);
+
if (!el) {
return;
}
+ const { stages, fullPath, iid } = el.dataset;
+
// Some commits have no pipeline, code splitting to load the pipeline optionally
- const { stages } = el.dataset;
- const { default: PipelineMiniGraph } = await import(
- /* webpackChunkName: 'pipelineMiniGraph' */ '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'
+ const { default: CommitBoxPipelineMiniGraph } = await import(
+ /* webpackChunkName: 'commitBoxPipelineMiniGraph' */ './components/commit_box_pipeline_mini_graph.vue'
);
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
+ provide: {
+ fullPath,
+ iid,
+ dataMethod: 'graphql',
+ },
render(createElement) {
- return createElement(PipelineMiniGraph, {
+ return createElement(CommitBoxPipelineMiniGraph, {
props: {
- stages: JSON.parse(stages),
+ // if stages do not exist for some reason, protect JSON.parse from erroring out
+ stages: stages ? JSON.parse(stages) : [],
},
});
},
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index f7cfc82db11..f2c1c843878 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -122,7 +122,7 @@ export default {
/>
</div>
<div class="gl-mt-4">
- <gl-button category="primary" variant="success" @click="onSubmit">
+ <gl-button category="primary" variant="confirm" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
<gl-button data-testid="swapRevisionsButton" class="btn btn-default" @click="onSwapRevision">
diff --git a/app/assets/javascripts/projects/compare/components/app_legacy.vue b/app/assets/javascripts/projects/compare/components/app_legacy.vue
deleted file mode 100644
index d3f09f7d69f..00000000000
--- a/app/assets/javascripts/projects/compare/components/app_legacy.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import csrf from '~/lib/utils/csrf';
-import RevisionDropdown from './revision_dropdown_legacy.vue';
-
-export default {
- csrf,
- components: {
- RevisionDropdown,
- GlButton,
- },
- props: {
- projectCompareIndexPath: {
- type: String,
- required: true,
- },
- refsProjectPath: {
- type: String,
- required: true,
- },
- paramsFrom: {
- type: String,
- required: false,
- default: null,
- },
- paramsTo: {
- type: String,
- required: false,
- default: null,
- },
- projectMergeRequestPath: {
- type: String,
- required: true,
- },
- createMrPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- from: this.paramsFrom,
- to: this.paramsTo,
- };
- },
- methods: {
- onSubmit() {
- this.$refs.form.submit();
- },
- onSwapRevision() {
- [this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
- },
- onSelectRevision({ direction, revision }) {
- this[direction] = revision; // direction is either 'from' or 'to'
- },
- },
-};
-</script>
-
-<template>
- <form
- ref="form"
- class="form-inline js-requires-input js-signature-container"
- method="POST"
- :action="projectCompareIndexPath"
- >
- <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
- <revision-dropdown
- :refs-project-path="refsProjectPath"
- revision-text="Source"
- params-name="to"
- :params-branch="to"
- data-testid="sourceRevisionDropdown"
- @selectRevision="onSelectRevision"
- />
- <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div>
- <revision-dropdown
- :refs-project-path="refsProjectPath"
- revision-text="Target"
- params-name="from"
- :params-branch="from"
- data-testid="targetRevisionDropdown"
- @selectRevision="onSelectRevision"
- />
- <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
- {{ s__('CompareRevisions|Compare') }}
- </gl-button>
- <gl-button
- data-testid="swapRevisionsButton"
- class="btn btn-default gl-button gl-ml-3"
- @click="onSwapRevision"
- >
- {{ s__('CompareRevisions|Swap revisions') }}
- </gl-button>
- <gl-button
- v-if="projectMergeRequestPath"
- :href="projectMergeRequestPath"
- data-testid="projectMrButton"
- class="btn btn-default gl-button gl-ml-3"
- >
- {{ s__('CompareRevisions|View open merge request') }}
- </gl-button>
- <gl-button
- v-else-if="createMrPath"
- :href="createMrPath"
- data-testid="createMrButton"
- class="btn btn-default gl-button gl-ml-3"
- >
- {{ s__('CompareRevisions|Create merge request') }}
- </gl-button>
- </form>
-</template>
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
index 322dff773b8..e485a086d39 100644
--- a/app/assets/javascripts/projects/compare/index.js
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -1,44 +1,9 @@
import Vue from 'vue';
import CompareApp from './components/app.vue';
-import CompareAppLegacy from './components/app_legacy.vue';
export default function init() {
const el = document.getElementById('js-compare-selector');
- if (gon.features?.compareRepoDropdown) {
- const {
- refsProjectPath,
- paramsFrom,
- paramsTo,
- projectCompareIndexPath,
- projectMergeRequestPath,
- createMrPath,
- projectTo,
- projectsFrom,
- } = el.dataset;
-
- return new Vue({
- el,
- components: {
- CompareApp,
- },
- render(createElement) {
- return createElement(CompareApp, {
- props: {
- refsProjectPath,
- paramsFrom,
- paramsTo,
- projectCompareIndexPath,
- projectMergeRequestPath,
- createMrPath,
- defaultProject: JSON.parse(projectTo),
- projects: JSON.parse(projectsFrom),
- },
- });
- },
- });
- }
-
const {
refsProjectPath,
paramsFrom,
@@ -46,15 +11,17 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
+ projectTo,
+ projectsFrom,
} = el.dataset;
return new Vue({
el,
components: {
- CompareAppLegacy,
+ CompareApp,
},
render(createElement) {
- return createElement(CompareAppLegacy, {
+ return createElement(CompareApp, {
props: {
refsProjectPath,
paramsFrom,
@@ -62,6 +29,8 @@ export default function init() {
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
+ defaultProject: JSON.parse(projectTo),
+ projects: JSON.parse(projectsFrom),
},
});
},
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 0b0560f63c1..d3cadcd2bd5 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -239,7 +239,7 @@ export default {
};
},
},
- successColor: '#608b2f',
+ successColor: '#366800',
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
diff --git a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
index 0b398eddc9c..02e31d6fbb3 100644
--- a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
+++ b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
@@ -1,7 +1,7 @@
<script>
import { GlBanner } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import { setCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
export default {
@@ -16,50 +16,36 @@ export default {
components: {
GlBanner,
},
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- },
+ inject: ['terraformImagePath', 'bannerDismissedKey'],
data() {
return {
isVisible: true,
};
},
computed: {
- bannerDissmisedKey() {
- return `terraform_notification_dismissed_for_project_${this.projectId}`;
- },
docsUrl() {
return helpPagePath('user/infrastructure/terraform_state');
},
},
- created() {
- if (parseBoolean(getCookie(this.bannerDissmisedKey))) {
- this.isVisible = false;
- }
- },
methods: {
handleClose() {
- setCookie(this.bannerDissmisedKey, true);
+ setCookie(this.bannerDismissedKey, true);
this.isVisible = false;
},
},
};
</script>
<template>
- <div v-if="isVisible">
- <div class="gl-py-5">
- <gl-banner
- :title="$options.i18n.title"
- :button-text="$options.i18n.buttonText"
- :button-link="docsUrl"
- variant="introduction"
- @close="handleClose"
- >
- <p>{{ $options.i18n.description }}</p>
- </gl-banner>
- </div>
+ <div v-if="isVisible" class="gl-py-5">
+ <gl-banner
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.buttonText"
+ :button-link="docsUrl"
+ :svg-path="terraformImagePath"
+ variant="promotion"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.description }}</p>
+ </gl-banner>
</div>
</template>
diff --git a/app/assets/javascripts/projects/terraform_notification/index.js b/app/assets/javascripts/projects/terraform_notification/index.js
index eb04f109a8e..0a273247930 100644
--- a/app/assets/javascripts/projects/terraform_notification/index.js
+++ b/app/assets/javascripts/projects/terraform_notification/index.js
@@ -1,18 +1,23 @@
import Vue from 'vue';
+import { parseBoolean, getCookie } from '~/lib/utils/common_utils';
import TerraformNotification from './components/terraform_notification.vue';
export default () => {
const el = document.querySelector('.js-terraform-notification');
+ const bannerDismissedKey = 'terraform_notification_dismissed';
- if (!el) {
+ if (!el || parseBoolean(getCookie(bannerDismissedKey))) {
return false;
}
- const { projectId } = el.dataset;
+ const { terraformImagePath } = el.dataset;
return new Vue({
el,
- render: (createElement) =>
- createElement(TerraformNotification, { props: { projectId: Number(projectId) } }),
+ provide: {
+ terraformImagePath,
+ bannerDismissedKey,
+ },
+ render: (createElement) => createElement(TerraformNotification),
});
};
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
index 0432cf1123c..f857c96c9d1 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { GlModal, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlFormInput } from '@gitlab/ui';
import { n__ } from '~/locale';
import {
REMOVE_TAG_CONFIRMATION_TEXT,
@@ -12,6 +12,7 @@ export default {
components: {
GlModal,
GlSprintf,
+ GlFormInput,
},
props: {
itemsToBeDeleted: {
@@ -25,7 +26,15 @@ export default {
required: false,
},
},
+ data() {
+ return {
+ projectPath: '',
+ };
+ },
computed: {
+ imageProjectPath() {
+ return this.itemsToBeDeleted[0]?.project?.path;
+ },
modalTitle() {
if (this.deleteImage) {
return DELETE_IMAGE_CONFIRMATION_TITLE;
@@ -40,6 +49,7 @@ export default {
if (this.deleteImage) {
return {
message: DELETE_IMAGE_CONFIRMATION_TEXT,
+ item: this.imageProjectPath,
};
}
if (this.itemsToBeDeleted.length > 1) {
@@ -55,6 +65,9 @@ export default {
item: first?.path,
};
},
+ disablePrimaryButton() {
+ return this.deleteImage && this.projectPath !== this.imageProjectPath;
+ },
},
methods: {
show() {
@@ -69,10 +82,14 @@ export default {
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
- :action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
+ :action-primary="{
+ text: __('Delete'),
+ attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
+ }"
:action-cancel="{ text: __('Cancel') }"
@primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
+ @change="projectPath = ''"
>
<template #modal-title>{{ modalTitle }}</template>
<p v-if="modalDescription" data-testid="description">
@@ -80,7 +97,13 @@ export default {
<template #item>
<b>{{ modalDescription.item }}</b>
</template>
+ <template #code>
+ <code>{{ modalDescription.item }}</code>
+ </template>
</gl-sprintf>
</p>
+ <div v-if="deleteImage">
+ <gl-form-input v-model="projectPath" />
+ </div>
</gl-modal>
</template>
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 80ed9a32039..e9e36151fe6 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,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective, GlDropdown, GlDropdownItem } from '@gitlab/ui';
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';
@@ -27,7 +27,7 @@ import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_cont
export default {
name: 'DetailsHeader',
- components: { GlButton, GlIcon, TitleArea, MetadataItem },
+ components: { GlIcon, TitleArea, MetadataItem, GlDropdown, GlDropdownItem },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -143,9 +143,22 @@ export default {
/>
</template>
<template #right-actions>
- <gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
- {{ __('Delete image repository') }}
- </gl-button>
+ <gl-dropdown
+ icon="ellipsis_v"
+ text="More actions"
+ :text-sr-only="true"
+ category="tertiary"
+ no-caret
+ right
+ >
+ <gl-dropdown-item
+ variant="danger"
+ :disabled="deleteButtonDisabled"
+ @click="$emit('delete')"
+ >
+ {{ __('Delete image repository') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
</title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
index 8d9e221af4c..1f52e319ad0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
@@ -34,7 +34,7 @@ export default {
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
},
computed: {
showStatus() {
@@ -61,7 +61,7 @@ export default {
</span>
<gl-icon
v-if="failedDelete"
- v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
+ v-gl-tooltip="{ title: $options.i18n.CLEANUP_TIMED_OUT_ERROR_MESSAGE }"
:size="14"
class="gl-text-black-normal"
data-testid="extra-info"
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index 9b4c06349e2..0836260b71e 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -99,7 +99,7 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
- 'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone.',
+ 'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}',
);
export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
@@ -162,6 +162,9 @@ export const IMAGE_STATUS_ALERT_TYPE = {
[DELETE_FAILED]: 'warning',
};
-export const PACKAGE_DELETE_HELP_PAGE_PATH = helpPagePath('user/packages/container_registry', {
- anchor: 'delete-images',
-});
+export const PACKAGE_DELETE_HELP_PAGE_PATH = helpPagePath(
+ 'user/packages/container_registry/index',
+ {
+ anchor: 'delete-images',
+ },
+);
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 88c2e667afd..b5a99fd9ac1 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
@@ -12,6 +12,7 @@ query getContainerRepositoryDetails($id: ID!) {
expirationPolicyCleanupStatus
project {
visibility
+ path
containerExpirationPolicy {
enabled
nextRunAt
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 34ec3b085a5..feabc4f770b 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -161,7 +161,7 @@ export default {
},
deleteImage() {
this.deleteImageAlert = true;
- this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
+ this.itemsToBeDeleted = [{ ...this.containerRepository }];
this.$refs.deleteModal.show();
},
deleteImageError() {
@@ -188,7 +188,7 @@ export default {
<partial-cleanup-alert
v-if="showPartialCleanupWarning"
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
- :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
+ :cleanup-policies-help-page-path="config.expirationPolicyHelpPagePath"
@dismiss="dismissPartialCleanupWarning"
/>
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 e568950380e..0e18d0992cd 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
@@ -12,19 +12,10 @@ export default {
ReportSection,
},
props: {
- headPath: {
- type: String,
- required: true,
- },
headBlobPath: {
type: String,
required: true,
},
- basePath: {
- type: String,
- required: false,
- default: null,
- },
baseBlobPath: {
type: String,
required: false,
@@ -52,8 +43,6 @@ export default {
},
created() {
this.setPaths({
- basePath: this.basePath,
- headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
reportsPath: this.codequalityReportsPath,
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
index e3238207af2..04aca11b945 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -1,4 +1,5 @@
-import axios from '~/lib/utils/axios_utils';
+import pollUntilComplete from '~/lib/utils/poll_until_complete';
+import { STATUS_NOT_FOUND } from '../../constants';
import * as types from './mutation_types';
import { parseCodeclimateMetrics } from './utils/codequality_parser';
@@ -7,12 +8,11 @@ export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
export const fetchReports = ({ state, dispatch, commit }) => {
commit(types.REQUEST_REPORTS);
- if (!state.basePath) {
- return dispatch('receiveReportsError');
- }
- return axios
- .get(state.reportsPath)
+ return pollUntilComplete(state.reportsPath)
.then(({ data }) => {
+ if (data.status === STATUS_NOT_FOUND) {
+ return dispatch('receiveReportsError', data);
+ }
return dispatch('receiveReportsSuccess', {
newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
index c6935291af2..3fb8c5be351 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -1,6 +1,6 @@
import { spriteIcon } from '~/lib/utils/common_utils';
import { sprintf, __, s__, n__ } from '~/locale';
-import { LOADING, ERROR, SUCCESS } from '../../constants';
+import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '../../constants';
export const hasCodequalityIssues = (state) =>
Boolean(state.newIssues?.length || state.resolvedIssues?.length);
@@ -42,7 +42,7 @@ export const codequalityText = (state) => {
};
export const codequalityPopover = (state) => {
- if (state.headPath && !state.basePath) {
+ if (state.status === STATUS_NOT_FOUND) {
return {
title: s__('ciReport|Base pipeline codequality artifact not found'),
content: sprintf(
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
index 095e6637966..249c2f35c0b 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -2,8 +2,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;
@@ -14,6 +12,7 @@ export default {
},
[types.RECEIVE_REPORTS_SUCCESS](state, data) {
state.hasError = false;
+ state.status = '';
state.statusReason = '';
state.isLoading = false;
state.newIssues = data.newIssues;
@@ -22,6 +21,7 @@ export default {
[types.RECEIVE_REPORTS_ERROR](state, error) {
state.isLoading = false;
state.hasError = true;
+ state.status = error?.status || '';
state.statusReason = error?.response?.data?.status_reason;
},
};
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js
index b39ff4f9d66..f68dbc2a5fa 100644
--- a/app/assets/javascripts/reports/codequality_report/store/state.js
+++ b/app/assets/javascripts/reports/codequality_report/store/state.js
@@ -1,6 +1,4 @@
export default () => ({
- basePath: null,
- headPath: null,
reportsPath: null,
baseBlobPath: null,
@@ -8,6 +6,7 @@ export default () => ({
isLoading: false,
hasError: false,
+ status: '',
statusReason: '',
newIssues: [],
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index 7f7ea2adc0e..53273aeff33 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -12,6 +12,7 @@ export const SUCCESS = 'SUCCESS';
export const STATUS_FAILED = 'failed';
export const STATUS_SUCCESS = 'success';
export const STATUS_NEUTRAL = 'neutral';
+export const STATUS_NOT_FOUND = 'not_found';
export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
diff --git a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue b/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
index af93e5bc639..ca518aea743 100644
--- a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
+++ b/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
@@ -47,7 +47,7 @@ export default {
<div v-for="(field, key, index) in filteredModalData" :key="index" class="row gl-mt-3 gl-mb-3">
<strong class="col-sm-3 text-right"> {{ field.text }}: </strong>
- <div class="col-sm-9 text-secondary">
+ <div class="col-sm-9">
<code-block v-if="field.type === $options.fieldTypes.codeBlock" :code="field.value" />
<gl-link
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 273825b996a..4e7ca7b17e4 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -2,6 +2,7 @@
import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getRefMixin from '../mixins/get_ref';
import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
@@ -17,11 +18,12 @@ export default {
GlButton,
UploadBlobModal,
DeleteBlobModal,
+ LockButton: () => import('ee_component/repository/components/lock_button.vue'),
},
directives: {
GlModal: GlModalDirective,
},
- mixins: [getRefMixin],
+ mixins: [getRefMixin, glFeatureFlagMixin()],
inject: {
targetBranch: {
default: '',
@@ -55,6 +57,18 @@ export default {
type: Boolean,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ isLocked: {
+ type: Boolean,
+ required: true,
+ },
+ canLock: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
replaceModalId() {
@@ -76,10 +90,19 @@ export default {
<template>
<div class="gl-mr-3">
<gl-button-group>
- <gl-button v-gl-modal="replaceModalId">
+ <lock-button
+ v-if="glFeatures.fileLocks"
+ :name="name"
+ :path="path"
+ :project-path="projectPath"
+ :is-locked="isLocked"
+ :can-lock="canLock"
+ data-testid="lock"
+ />
+ <gl-button v-gl-modal="replaceModalId" data-testid="replace">
{{ $options.i18n.replace }}
</gl-button>
- <gl-button v-gl-modal="deleteModalId">
+ <gl-button v-gl-modal="deleteModalId" data-testid="delete">
{{ $options.i18n.delete }}
</gl-button>
</gl-button-group>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 09ac60c94c7..665b0698cc0 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -8,6 +8,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import getRefMixin from '../mixins/get_ref';
import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue';
@@ -21,6 +22,12 @@ export default {
BlobContent,
GlLoadingIcon,
},
+ mixins: [getRefMixin],
+ inject: {
+ originalBranch: {
+ default: '',
+ },
+ },
apollo: {
project: {
query: blobInfoQuery,
@@ -28,6 +35,7 @@ export default {
return {
projectPath: this.projectPath,
filePath: this.path,
+ ref: this.originalBranch || this.ref,
};
},
result() {
@@ -67,6 +75,10 @@ export default {
project: {
userPermissions: {
pushCode: false,
+ downloadCode: false,
+ },
+ pathLocks: {
+ nodes: [],
},
repository: {
empty: true,
@@ -87,9 +99,6 @@ export default {
externalStorageUrl: '',
replacePath: '',
deletePath: '',
- canLock: false,
- isLocked: false,
- lockLink: '',
forkPath: '',
simpleViewer: {},
richViewer: null,
@@ -108,8 +117,11 @@ export default {
isLoading() {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
},
+ isBinaryFileType() {
+ return this.isBinary || this.viewer.fileType === 'download';
+ },
blobInfo() {
- const nodes = this.project?.repository?.blobs?.nodes;
+ const nodes = this.project?.repository?.blobs?.nodes || [];
return nodes[0] || {};
},
@@ -131,6 +143,14 @@ export default {
const { fileType } = this.viewer;
return viewerProps(fileType, this.blobInfo);
},
+ canLock() {
+ const { pushCode, downloadCode } = this.project.userPermissions;
+
+ return pushCode && downloadCode;
+ },
+ isLocked() {
+ return this.project.pathLocks.nodes.some((node) => node.path === this.path);
+ },
},
methods: {
loadLegacyViewer() {
@@ -161,13 +181,14 @@ export default {
<blob-header
:blob="blobInfo"
:hide-viewer-switcher="!hasRichViewer || isBinary"
+ :is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
>
<template #actions>
<blob-edit
- v-if="!isBinary"
+ :show-edit-button="!isBinary"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
/>
@@ -179,6 +200,9 @@ export default {
:delete-path="blobInfo.webPath"
:can-push-code="project.userPermissions.pushCode"
:empty-repo="project.repository.empty"
+ :project-path="projectPath"
+ :is-locked="isLocked"
+ :can-lock="canLock"
/>
</template>
</blob-header>
diff --git a/app/assets/javascripts/repository/components/blob_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue
index 3d97ebe89e4..30ed4cd57f1 100644
--- a/app/assets/javascripts/repository/components/blob_edit.vue
+++ b/app/assets/javascripts/repository/components/blob_edit.vue
@@ -15,6 +15,10 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ showEditButton: {
+ type: Boolean,
+ required: true,
+ },
editPath: {
type: String,
required: true,
@@ -30,17 +34,31 @@ export default {
<template>
<web-ide-link
v-if="glFeatures.consolidatedEditButton"
+ :show-edit-button="showEditButton"
class="gl-mr-3"
:edit-url="editPath"
:web-ide-url="webIdePath"
:is-blob="true"
/>
<div v-else>
- <gl-button class="gl-mr-2" category="primary" variant="confirm" :href="editPath">
+ <gl-button
+ v-if="showEditButton"
+ class="gl-mr-2"
+ category="primary"
+ variant="confirm"
+ :href="editPath"
+ data-testid="edit"
+ >
{{ $options.i18n.edit }}
</gl-button>
- <gl-button class="gl-mr-3" category="primary" variant="confirm" :href="webIdePath">
+ <gl-button
+ class="gl-mr-3"
+ category="primary"
+ variant="confirm"
+ :href="webIdePath"
+ data-testid="web-ide"
+ >
{{ $options.i18n.webIde }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 0b8408643ac..db84e2b5912 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -247,7 +247,8 @@ export default {
return items;
},
renderAddToTreeDropdown() {
- return this.canCollaborate || this.canCreateMrFromFork;
+ const isBlobPath = this.$route.name === 'blobPath' || this.$route.name === 'blobPathDecoded';
+ return !isBlobPath && (this.canCollaborate || this.canCreateMrFromFork);
},
},
methods: {
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 6599d99d7bd..a307b7c0b8a 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -1,14 +1,24 @@
<script>
-import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle } from '@gitlab/ui';
+import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __ } from '~/locale';
+import validation from '~/vue_shared/directives/validation';
import {
SECONDARY_OPTIONS_TEXT,
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
+ COMMIT_MESSAGE_SUBJECT_MAX_LENGTH,
+ COMMIT_MESSAGE_BODY_MAX_LENGTH,
} from '../constants';
+const initFormField = ({ value, required = true, skipValidation = false }) => ({
+ value,
+ required,
+ state: skipValidation ? true : null,
+ feedback: null,
+});
+
export default {
csrf,
components: {
@@ -17,6 +27,7 @@ export default {
GlFormInput,
GlFormTextarea,
GlToggle,
+ GlForm,
},
i18n: {
PRIMARY_OPTIONS_TEXT: __('Delete file'),
@@ -24,6 +35,12 @@ export default {
COMMIT_LABEL,
TARGET_BRANCH_LABEL,
TOGGLE_CREATE_MR_LABEL,
+ COMMIT_MESSAGE_HINT: __(
+ 'Try to keep the first line under 52 characters and the others under 72.',
+ ),
+ },
+ directives: {
+ validation: validation(),
},
props: {
modalId: {
@@ -60,12 +77,20 @@ export default {
},
},
data() {
+ const form = {
+ state: false,
+ showValidation: false,
+ fields: {
+ // fields key must match case of form name for validation directive to work
+ commit_message: initFormField({ value: this.commitMessage }),
+ branch_name: initFormField({ value: this.targetBranch }),
+ },
+ };
return {
loading: false,
- commit: this.commitMessage,
- target: this.targetBranch,
createNewMr: true,
error: '',
+ form,
};
},
computed: {
@@ -76,7 +101,7 @@ export default {
{
variant: 'danger',
loading: this.loading,
- disabled: !this.formCompleted || this.loading,
+ disabled: this.loading || !this.form.state,
},
],
};
@@ -91,18 +116,44 @@ export default {
],
};
},
+ /* eslint-disable dot-notation */
showCreateNewMrToggle() {
- return this.canPushCode && this.target !== this.originalBranch;
+ return this.canPushCode && this.form.fields['branch_name'].value !== this.originalBranch;
},
formCompleted() {
- return this.commit && this.target;
+ return this.form.fields['commit_message'].value && this.form.fields['branch_name'].value;
},
+ showHint() {
+ const splitCommitMessageByLineBreak = this.form.fields['commit_message'].value
+ .trim()
+ .split('\n');
+ const [firstLine, ...otherLines] = splitCommitMessageByLineBreak;
+
+ const hasFirstLineExceedMaxLength = firstLine.length > COMMIT_MESSAGE_SUBJECT_MAX_LENGTH;
+
+ const hasOtherLineExceedMaxLength =
+ Boolean(otherLines.length) &&
+ otherLines.some((text) => text.length > COMMIT_MESSAGE_BODY_MAX_LENGTH);
+
+ return (
+ !this.form.fields['commit_message'].feedback &&
+ (hasFirstLineExceedMaxLength || hasOtherLineExceedMaxLength)
+ );
+ },
+ /* eslint-enable dot-notation */
},
methods: {
submitForm(e) {
e.preventDefault(); // Prevent modal from closing
+ this.form.showValidation = true;
+
+ if (!this.form.state) {
+ return;
+ }
+
this.loading = true;
- this.$refs.form.submit();
+ this.form.showValidation = false;
+ this.$refs.form.$el.submit();
},
},
};
@@ -110,13 +161,15 @@ export default {
<template>
<gl-modal
+ v-bind="$attrs"
+ data-testid="modal-delete"
:modal-id="modalId"
:title="modalTitle"
:action-primary="primaryOptions"
:action-cancel="cancelOptions"
@primary="submitForm"
>
- <form ref="form" :action="deletePath" method="post">
+ <gl-form ref="form" novalidate :action="deletePath" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<template v-if="emptyRepo">
@@ -129,15 +182,37 @@ export default {
<!-- Once "push to branch" permission is made available, will need to add to conditional
Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 -->
<input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
- <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
- <gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
+ <gl-form-group
+ :label="$options.i18n.COMMIT_LABEL"
+ label-for="commit_message"
+ :invalid-feedback="form.fields['commit_message'].feedback"
+ >
+ <gl-form-textarea
+ v-model="form.fields['commit_message'].value"
+ v-validation:[form.showValidation]
+ name="commit_message"
+ :state="form.fields['commit_message'].state"
+ :disabled="loading"
+ required
+ />
+ <p v-if="showHint" class="form-text gl-text-gray-600" data-testid="hint">
+ {{ $options.i18n.COMMIT_MESSAGE_HINT }}
+ </p>
</gl-form-group>
<gl-form-group
v-if="canPushCode"
:label="$options.i18n.TARGET_BRANCH_LABEL"
label-for="branch_name"
+ :invalid-feedback="form.fields['branch_name'].feedback"
>
- <gl-form-input v-model="target" :disabled="loading" name="branch_name" />
+ <gl-form-input
+ v-model="form.fields['branch_name'].value"
+ v-validation:[form.showValidation]
+ :state="form.fields['branch_name'].state"
+ :disabled="loading"
+ name="branch_name"
+ required
+ />
</gl-form-group>
<gl-toggle
v-if="showCreateNewMrToggle"
@@ -146,6 +221,6 @@ export default {
:label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
/>
</template>
- </form>
+ </gl-form>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 82c18d13a6a..fa358a75cc1 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -170,6 +170,7 @@ export default {
this.apolloQuery(blobInfoQuery, {
projectPath: this.projectPath,
filePath: this.path,
+ ref: this.ref,
});
},
apolloQuery(query, variables) {
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 2d2faa8d9f3..b536bcb1875 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -8,3 +8,6 @@ export const SECONDARY_OPTIONS_TEXT = __('Cancel');
export const COMMIT_LABEL = __('Commit message');
export const TARGET_BRANCH_LABEL = __('Target branch');
export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
+
+export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52;
+export const COMMIT_MESSAGE_BODY_MAX_LENGTH = 72;
diff --git a/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql
new file mode 100644
index 00000000000..eaebc4ddf17
--- /dev/null
+++ b/app/assets/javascripts/repository/mutations/lock_path.mutation.graphql
@@ -0,0 +1,13 @@
+mutation toggleLock($projectPath: ID!, $filePath: String!, $lock: Boolean!) {
+ projectSetLocked(input: { projectPath: $projectPath, filePath: $filePath, lock: $lock }) {
+ project {
+ id
+ pathLocks {
+ nodes {
+ path
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index a8f263941e2..45f07f7dc58 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -1,11 +1,18 @@
-query getBlobInfo($projectPath: ID!, $filePath: String!) {
+query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
project(fullPath: $projectPath) {
+ id
userPermissions {
pushCode
+ downloadCode
+ }
+ pathLocks {
+ nodes {
+ path
+ }
}
repository {
empty
- blobs(paths: [$filePath]) {
+ blobs(paths: [$filePath], ref: $ref) {
nodes {
webPath
name
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index 8d39243d609..23ecee449a4 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -9,15 +9,15 @@ import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
-import { captureException } from '../sentry_utils';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
-} from './runner_search_utils';
+} from '../runner_search_utils';
+import { captureException } from '../sentry_utils';
export default {
- name: 'RunnerListApp',
+ name: 'AdminRunnersApp',
components: {
RunnerFilteredSearchBar,
RunnerList,
diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 16616f00d1e..1eec1019b73 100644
--- a/app/assets/javascripts/runner/runner_list/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RunnerDetailsApp from './runner_list_app.vue';
+import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(VueApollo);
-export const initRunnerList = (selector = '#js-runner-list') => {
+export const initAdminRunners = (selector = '#js-admin-runners') => {
const el = document.querySelector(selector);
if (!el) {
@@ -32,7 +32,7 @@ export const initRunnerList = (selector = '#js-runner-list') => {
runnerInstallHelpPage,
},
render(h) {
- return h(RunnerDetailsApp, {
+ return h(AdminRunnersApp, {
props: {
activeRunnersCount: parseInt(activeRunnersCount, 10),
registrationToken,
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
index 2335faa4f85..cdf14abd4f9 100644
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -1,6 +1,8 @@
<script>
import { GlButton } from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
+import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
@@ -11,6 +13,14 @@ export default {
components: {
GlButton,
},
+ inject: {
+ groupId: {
+ default: null,
+ },
+ projectId: {
+ default: null,
+ },
+ },
props: {
type: {
type: String,
@@ -25,7 +35,28 @@ export default {
loading: false,
};
},
- computed: {},
+ computed: {
+ resetTokenInput() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return {
+ type: this.type,
+ };
+ case GROUP_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_GROUP, this.groupId),
+ type: this.type,
+ };
+ case PROJECT_TYPE:
+ return {
+ id: convertToGraphQLId(TYPE_PROJECT, this.projectId),
+ type: this.type,
+ };
+ default:
+ return null;
+ }
+ },
+ },
methods: {
async resetToken() {
// TODO Replace confirmation with gl-modal
@@ -44,13 +75,7 @@ export default {
} = await this.$apollo.mutate({
mutation: runnersRegistrationTokenResetMutation,
variables: {
- // TODO Currently INTANCE_TYPE only is supported
- // In future iterations this component will support
- // other registration token types.
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/19819
- input: {
- type: this.type,
- },
+ input: this.resetTokenInput,
},
});
if (errors && errors.length) {
diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue
index 72ce582e02c..aa435aaa823 100644
--- a/app/assets/javascripts/runner/components/runner_type_alert.vue
+++ b/app/assets/javascripts/runner/components/runner_type_alert.vue
@@ -6,28 +6,19 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
const ALERT_DATA = {
[INSTANCE_TYPE]: {
- title: s__(
- 'Runners|This runner is available to all groups and projects in your GitLab instance.',
- ),
message: s__(
- 'Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.',
+ 'Runners|This runner is available to all groups and projects in your GitLab instance.',
),
variant: 'success',
anchor: 'shared-runners',
},
[GROUP_TYPE]: {
- title: s__('Runners|This runner is available to all projects and subgroups in a group.'),
- message: s__(
- 'Runners|Use Group runners when you want all projects in a group to have access to a set of runners.',
- ),
+ message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
variant: 'success',
anchor: 'group-runners',
},
[PROJECT_TYPE]: {
- title: s__('Runners|This runner is associated with specific projects.'),
- message: s__(
- 'Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner.',
- ),
+ message: s__('Runners|This runner is associated with one or more projects.'),
variant: 'info',
anchor: 'specific-runners',
},
@@ -59,7 +50,7 @@ export default {
};
</script>
<template>
- <gl-alert v-if="alert" :variant="alert.variant" :title="alert.title" :dismissible="false">
+ <gl-alert v-if="alert" :variant="alert.variant" :dismissible="false">
{{ alert.message }}
<gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
</gl-alert>
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 85d14547efd..a5bc1680852 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -111,7 +111,7 @@ export default {
>
{{ __('Paused') }}
<template #help>
- {{ __("Paused runners don't accept new jobs") }}
+ {{ s__('Runners|Stop the runner from accepting new jobs.') }}
</template>
</gl-form-checkbox>
@@ -123,14 +123,14 @@ export default {
>
{{ __('Protected') }}
<template #help>
- {{ __('This runner will only run on pipelines triggered on protected branches') }}
+ {{ s__('Runners|Use the runner on pipelines for protected branches only.') }}
</template>
</gl-form-checkbox>
<gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged">
{{ __('Run untagged jobs') }}
<template #help>
- {{ __('Indicates whether this runner can pick jobs without tags') }}
+ {{ s__('Runners|Use the runner for jobs without tags, in addition to tagged jobs.') }}
</template>
</gl-form-checkbox>
@@ -141,7 +141,7 @@ export default {
>
{{ __('Lock to current projects') }}
<template #help>
- {{ __('When a runner is locked, it cannot be assigned to other projects') }}
+ {{ s__('Runners|Use the runner for the currently assigned projects only.') }}
</template>
</gl-form-checkbox>
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index 0c69072f06a..51fae60b6b7 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -28,11 +28,6 @@ export default {
};
},
methods: {
- fnCurrentTokenValue(data) {
- // By default, values are transformed with `toLowerCase`
- // however, runner tags are case sensitive.
- return data;
- },
getTagsOptions(search) {
// TODO This should be implemented via a GraphQL API
// The API should
@@ -72,7 +67,6 @@ export default {
:config="config"
:suggestions-loading="loading"
:suggestions="tags"
- :fn-current-token-value="fnCurrentTokenValue"
:recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
@fetch-suggestions="fetchTags"
v-on="$listeners"
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
new file mode 100644
index 00000000000..07bbf60c453
--- /dev/null
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -0,0 +1,35 @@
+<script>
+import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
+import RunnerTypeHelp from '../components/runner_type_help.vue';
+import { GROUP_TYPE } from '../constants';
+
+export default {
+ components: {
+ RunnerManualSetupHelp,
+ RunnerTypeHelp,
+ },
+ props: {
+ registrationToken: {
+ type: String,
+ required: true,
+ },
+ },
+ GROUP_TYPE,
+};
+</script>
+
+<template>
+ <div>
+ <div class="row">
+ <div class="col-sm-6">
+ <runner-type-help />
+ </div>
+ <div class="col-sm-6">
+ <runner-manual-setup-help
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
new file mode 100644
index 00000000000..e14c583d73e
--- /dev/null
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import GroupRunnersApp from './group_runners_app.vue';
+
+Vue.use(VueApollo);
+
+export const initGroupRunners = (selector = '#js-group-runners') => {
+ const el = document.querySelector(selector);
+
+ if (!el) {
+ return null;
+ }
+
+ const { registrationToken, groupId } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ groupId,
+ },
+ render(h) {
+ return h(GroupRunnersApp, {
+ props: {
+ registrationToken,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/runner/runner_list/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 9a0dc9c3a32..65f75eb11ac 100644
--- a/app/assets/javascripts/runner/runner_list/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -16,7 +16,7 @@ import {
PARAM_KEY_BEFORE,
DEFAULT_SORT,
RUNNER_PAGE_SIZE,
-} from '../constants';
+} from './constants';
const getPaginationFromParams = (params) => {
const page = parseInt(params[PARAM_KEY_PAGE], 10);
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index b53557c0ec5..ee5e778f63d 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -46,38 +46,44 @@ export const fetchProjects = ({ commit, state }, search) => {
}
};
-export const loadFrequentGroups = async ({ commit }) => {
- const data = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
- commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data });
+export const preloadStoredFrequentItems = ({ commit }) => {
+ const storedGroups = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: storedGroups });
- const promises = data.map((d) => Api.group(d.id));
+ const storedProjects = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: storedProjects });
+};
+
+export const loadFrequentGroups = async ({ commit, state }) => {
+ const storedData = state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
+ const promises = storedData.map((d) => Api.group(d.id));
try {
- const inflatedData = mergeById(await Promise.all(promises), data);
+ const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
createFlash({ message: __('There was a problem fetching recent groups.') });
}
};
-export const loadFrequentProjects = async ({ commit }) => {
- const data = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
- commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data });
-
- const promises = data.map((d) => Api.project(d.id).then((res) => res.data));
+export const loadFrequentProjects = async ({ commit, state }) => {
+ const storedData = state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY];
+ const promises = storedData.map((d) => Api.project(d.id).then((res) => res.data));
try {
- const inflatedData = mergeById(await Promise.all(promises), data);
+ const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
createFlash({ message: __('There was a problem fetching recent projects.') });
}
};
-export const setFrequentGroup = ({ state }, item) => {
- setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+export const setFrequentGroup = ({ state, commit }, item) => {
+ const frequentItems = setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: frequentItems });
};
-export const setFrequentProject = ({ state }, item) => {
- setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+export const setFrequentProject = ({ state, commit }, item) => {
+ const frequentItems = setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
};
export const setQuery = ({ commit }, { key, value }) => {
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index 60c09221ca9..b7d97213594 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -21,7 +21,7 @@ export const loadDataFromLS = (key) => {
export const setFrequentItemToLS = (key, data, itemData) => {
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
- return;
+ return [];
}
const keyList = [
@@ -66,9 +66,11 @@ export const setFrequentItemToLS = (key, data, itemData) => {
// Note we do not need to commit a mutation here as immediately after this we refresh the page to
// update the search results.
localStorage.setItem(key, JSON.stringify(frequentItems));
+ return frequentItems;
} catch {
// The LS got in a bad state, let's wipe it
localStorage.removeItem(key);
+ return [];
}
};
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index a490adbc11a..65114ee066e 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -39,8 +39,11 @@ export default {
return !this.query.snippets || this.query.snippets === 'false';
},
},
+ created() {
+ this.preloadStoredFrequentItems();
+ },
methods: {
- ...mapActions(['applyQuery', 'setQuery']),
+ ...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
},
};
</script>
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
index 45a6ae73fac..e5edb21792a 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -18,12 +18,18 @@ export default {
},
},
computed: {
- ...mapState(['groups', 'fetchingGroups']),
+ ...mapState(['query', 'groups', 'fetchingGroups']),
...mapGetters(['frequentGroups']),
selectedGroup() {
return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
},
},
+ created() {
+ // This tracks groups searched via the top nav search bar
+ if (this.query.nav_source === 'navbar' && this.initialData?.id) {
+ this.setFrequentGroup(this.initialData);
+ }
+ },
methods: {
...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
handleGroupChange(group) {
@@ -33,7 +39,11 @@ export default {
}
visitUrl(
- setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }),
+ setUrlParams({
+ [GROUP_DATA.queryParam]: group.id,
+ [PROJECT_DATA.queryParam]: null,
+ nav_source: null,
+ }),
);
},
},
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
index 1ca31db61e5..85cf2ddbbff 100644
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -17,12 +17,18 @@ export default {
},
},
computed: {
- ...mapState(['projects', 'fetchingProjects']),
+ ...mapState(['query', 'projects', 'fetchingProjects']),
...mapGetters(['frequentProjects']),
selectedProject() {
return this.initialData ? this.initialData : ANY_OPTION;
},
},
+ created() {
+ // This tracks projects searched via the top nav search bar
+ if (this.query.nav_source === 'navbar' && this.initialData?.id) {
+ this.setFrequentProject(this.initialData);
+ }
+ },
methods: {
...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
handleProjectChange(project) {
@@ -35,6 +41,7 @@ export default {
const queryParams = {
...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
[PROJECT_DATA.queryParam]: project.id,
+ nav_source: null,
};
visitUrl(setUrlParams(queryParams));
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 4f278677c5f..b2bf913fe45 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -284,8 +284,8 @@ export class SearchAutocomplete {
if (projectId) {
const projectOptions = gl.projectOptions[getProjectSlug()];
const url = groupId
- ? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}`
- : `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}`;
+ ? `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&group_id=${groupId}&nav_source=navbar`
+ : `${gon.relative_url_root}/search?search=${term}&project_id=${projectId}&nav_source=navbar`;
options.push({
icon,
@@ -313,7 +313,7 @@ export class SearchAutocomplete {
},
false,
),
- url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}`,
+ url: `${gon.relative_url_root}/search?search=${term}&group_id=${groupId}&nav_source=navbar`,
});
}
@@ -321,7 +321,7 @@ export class SearchAutocomplete {
icon,
text: term,
template: s__('SearchAutocomplete|in all GitLab'),
- url: `${gon.relative_url_root}/search?search=${term}`,
+ url: `${gon.relative_url_root}/search?search=${term}&nav_source=navbar`,
});
return options;
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 513a7353d28..6c70a8c33db 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,23 +1,216 @@
<script>
-import ConfigurationTable from './configuration_table.vue';
+import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
+import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
+import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
+import FeatureCard from './feature_card.vue';
+import SectionLayout from './section_layout.vue';
+import UpgradeBanner from './upgrade_banner.vue';
+
+export const i18n = {
+ compliance: s__('SecurityConfiguration|Compliance'),
+ configurationHistory: s__('SecurityConfiguration|Configuration history'),
+ securityTesting: s__('SecurityConfiguration|Security testing'),
+ latestPipelineDescription: s__(
+ `SecurityConfiguration|The status of the tools only applies to the
+ default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.`,
+ ),
+ description: s__(
+ `SecurityConfiguration|Once you've enabled a scan for the default branch,
+ any subsequent feature branch you create will include the scan.`,
+ ),
+ securityConfiguration: __('Security Configuration'),
+};
export default {
+ i18n,
components: {
- ConfigurationTable,
+ AutoDevOpsAlert,
+ AutoDevOpsEnabledAlert,
+ FeatureCard,
+ GlLink,
+ GlSprintf,
+ GlTab,
+ GlTabs,
+ LocalStorageSync,
+ SectionLayout,
+ UpgradeBanner,
+ UserCalloutDismisser,
+ },
+ inject: ['projectPath'],
+ props: {
+ augmentedSecurityFeatures: {
+ type: Array,
+ required: true,
+ },
+ augmentedComplianceFeatures: {
+ type: Array,
+ required: true,
+ },
+ gitlabCiPresent: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ autoDevopsEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canEnableAutoDevops: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ gitlabCiHistoryPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ latestPipelinePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ autoDevopsEnabledAlertDismissedProjects: [],
+ };
+ },
+ computed: {
+ canUpgrade() {
+ return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some(
+ ({ available }) => !available,
+ );
+ },
+ canViewCiHistory() {
+ return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
+ },
+ shouldShowDevopsAlert() {
+ return !this.autoDevopsEnabled && !this.gitlabCiPresent && this.canEnableAutoDevops;
+ },
+ shouldShowAutoDevopsEnabledAlert() {
+ return (
+ this.autoDevopsEnabled &&
+ !this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectPath)
+ );
+ },
+ },
+ methods: {
+ dismissAutoDevopsEnabledAlert() {
+ const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
+ dismissedProjects.add(this.projectPath);
+ this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
+ },
},
+ autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
};
</script>
<template>
<article>
+ <local-storage-sync
+ v-model="autoDevopsEnabledAlertDismissedProjects"
+ :storage-key="$options.autoDevopsEnabledAlertStorageKey"
+ as-json
+ />
+
+ <user-callout-dismisser
+ v-if="shouldShowDevopsAlert"
+ feature-name="security_configuration_devops_alert"
+ >
+ <template #default="{ dismiss, shouldShowCallout }">
+ <auto-dev-ops-alert v-if="shouldShowCallout" class="gl-mt-3" @dismiss="dismiss" />
+ </template>
+ </user-callout-dismisser>
<header>
- <h4 class="gl-my-5">
- {{ __('Security Configuration') }}
- </h4>
- <h5 class="gl-font-lg gl-mt-7">
- {{ s__('SecurityConfiguration|Testing & Compliance') }}
- </h5>
+ <h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
</header>
- <configuration-table />
+ <user-callout-dismisser v-if="canUpgrade" feature-name="security_configuration_upgrade_banner">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <upgrade-banner v-if="shouldShowCallout" @close="dismiss" />
+ </template>
+ </user-callout-dismisser>
+
+ <gl-tabs content-class="gl-pt-0">
+ <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
+ <auto-dev-ops-enabled-alert
+ v-if="shouldShowAutoDevopsEnabledAlert"
+ class="gl-mt-3"
+ @dismiss="dismissAutoDevopsEnabledAlert"
+ />
+
+ <section-layout :heading="$options.i18n.securityTesting">
+ <template #description>
+ <p>
+ <span data-testid="latest-pipeline-info-security">
+ <gl-sprintf
+ v-if="latestPipelinePath"
+ :message="$options.i18n.latestPipelineDescription"
+ >
+ <template #link="{ content }">
+ <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+
+ {{ $options.i18n.description }}
+ </p>
+ <p v-if="canViewCiHistory">
+ <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
+ $options.i18n.configurationHistory
+ }}</gl-link>
+ </p>
+ </template>
+
+ <template #features>
+ <feature-card
+ v-for="feature in augmentedSecurityFeatures"
+ :key="feature.type"
+ data-testid="security-testing-card"
+ :feature="feature"
+ class="gl-mb-6"
+ />
+ </template>
+ </section-layout>
+ </gl-tab>
+ <gl-tab data-testid="compliance-testing-tab" :title="$options.i18n.compliance">
+ <section-layout :heading="$options.i18n.compliance">
+ <template #description>
+ <p>
+ <span data-testid="latest-pipeline-info-compliance">
+ <gl-sprintf
+ v-if="latestPipelinePath"
+ :message="$options.i18n.latestPipelineDescription"
+ >
+ <template #link="{ content }">
+ <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+
+ {{ $options.i18n.description }}
+ </p>
+ <p v-if="canViewCiHistory">
+ <gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{
+ $options.i18n.configurationHistory
+ }}</gl-link>
+ </p>
+ </template>
+ <template #features>
+ <feature-card
+ v-for="feature in augmentedComplianceFeatures"
+ :key="feature.type"
+ :feature="feature"
+ class="gl-mb-6"
+ />
+ </template>
+ </section-layout>
+ </gl-tab>
+ </gl-tabs>
</article>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/auto_dev_ops_enabled_alert.vue b/app/assets/javascripts/security_configuration/components/auto_dev_ops_enabled_alert.vue
new file mode 100644
index 00000000000..7192108f7c5
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_enabled_alert.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['autoDevopsHelpPagePath'],
+ i18n: {
+ body: s__(
+ 'AutoDevopsAlert|Security testing tools enabled with %{linkStart}Auto DevOps%{linkEnd}',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="success" @dismiss="$emit('dismiss')">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="autoDevopsHelpPagePath">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
deleted file mode 100644
index 7f250bf1365..00000000000
--- a/app/assets/javascripts/security_configuration/components/configuration_table.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<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,
- REPORT_TYPE_DAST_PROFILES,
- REPORT_TYPE_DEPENDENCY_SCANNING,
- REPORT_TYPE_CONTAINER_SCANNING,
- REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
- REPORT_TYPE_COVERAGE_FUZZING,
- REPORT_TYPE_API_FUZZING,
- REPORT_TYPE_LICENSE_COMPLIANCE,
-} from '~/vue_shared/security_reports/constants';
-
-import { scanners } from './constants';
-import Upgrade from './upgrade.vue';
-
-const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
-const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
-
-export default {
- components: {
- GlLink,
- GlTable,
- GlAlert,
- },
- data() {
- return {
- errorMessage: '',
- };
- },
- methods: {
- getFeatureDocumentationLinkLabel(item) {
- return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
- featureName: item.name,
- });
- },
- onError(value) {
- this.errorMessage = value;
- },
- getComponentForItem(item) {
- const COMPONENTS = {
- [REPORT_TYPE_SAST]: ManageViaMR,
- [REPORT_TYPE_DAST]: Upgrade,
- [REPORT_TYPE_DAST_PROFILES]: Upgrade,
- [REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
- [REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
- [REPORT_TYPE_CLUSTER_IMAGE_SCANNING]: Upgrade,
- [REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
- [REPORT_TYPE_API_FUZZING]: Upgrade,
- [REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
- };
- return COMPONENTS[item.type];
- },
- },
- table: {
- fields: [
- {
- key: 'feature',
- label: s__('SecurityConfiguration|Security Control'),
- thClass,
- },
- {
- key: 'manage',
- label: s__('SecurityConfiguration|Manage'),
- thClass,
- },
- ],
- items: scanners,
- },
-};
-</script>
-
-<template>
- <div>
- <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
- {{ errorMessage }}
- </gl-alert>
- <gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
- <template #cell(feature)="{ item }">
- <div class="gl-text-gray-900">
- {{ item.name }}
- </div>
- <div>
- {{ item.description }}
- <gl-link
- target="_blank"
- data-testid="help-link"
- :href="item.helpPath"
- :aria-label="getFeatureDocumentationLinkLabel(item)"
- >
- {{ s__('SecurityConfiguration|More information') }}
- </gl-link>
- </div>
- </template>
-
- <template #cell(manage)="{ item }">
- <component
- :is="getComponentForItem(item)"
- :feature="item"
- :data-testid="item.type"
- @error="onError"
- />
- </template>
- </gl-table>
- </div>
-</template>
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 5cb9277040d..ebe0138f046 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -18,8 +18,9 @@ import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
/**
- * Translations & helpPagePaths for Static Security Configuration Page
+ * Translations & helpPagePaths for Security Configuration Page
*/
+
export const SAST_NAME = __('Static Application Security Testing (SAST)');
export const SAST_SHORT_NAME = s__('ciReport|SAST');
export const SAST_DESCRIPTION = __('Analyze your source code for known vulnerabilities.');
@@ -98,6 +99,10 @@ export const COVERAGE_FUZZING_DESCRIPTION = __(
export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
'user/application_security/coverage_fuzzing/index',
);
+export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+ { anchor: 'configuration' },
+);
export const API_FUZZING_NAME = __('API Fuzzing');
export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.');
@@ -111,73 +116,6 @@ export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index',
);
-export const UPGRADE_CTA = s__(
- 'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
-);
-
-export const scanners = [
- {
- name: SAST_NAME,
- description: SAST_DESCRIPTION,
- helpPath: SAST_HELP_PATH,
- type: REPORT_TYPE_SAST,
- },
- {
- name: DAST_NAME,
- description: DAST_DESCRIPTION,
- helpPath: DAST_HELP_PATH,
- type: REPORT_TYPE_DAST,
- },
- {
- name: DAST_PROFILES_NAME,
- description: DAST_PROFILES_DESCRIPTION,
- helpPath: DAST_PROFILES_HELP_PATH,
- type: REPORT_TYPE_DAST_PROFILES,
- },
- {
- name: DEPENDENCY_SCANNING_NAME,
- description: DEPENDENCY_SCANNING_DESCRIPTION,
- helpPath: DEPENDENCY_SCANNING_HELP_PATH,
- type: REPORT_TYPE_DEPENDENCY_SCANNING,
- },
- {
- name: CONTAINER_SCANNING_NAME,
- description: CONTAINER_SCANNING_DESCRIPTION,
- helpPath: CONTAINER_SCANNING_HELP_PATH,
- type: REPORT_TYPE_CONTAINER_SCANNING,
- },
- {
- name: CLUSTER_IMAGE_SCANNING_NAME,
- description: CLUSTER_IMAGE_SCANNING_DESCRIPTION,
- helpPath: CLUSTER_IMAGE_SCANNING_HELP_PATH,
- type: REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
- },
- {
- name: SECRET_DETECTION_NAME,
- description: SECRET_DETECTION_DESCRIPTION,
- helpPath: SECRET_DETECTION_HELP_PATH,
- type: REPORT_TYPE_SECRET_DETECTION,
- },
- {
- name: COVERAGE_FUZZING_NAME,
- description: COVERAGE_FUZZING_DESCRIPTION,
- helpPath: COVERAGE_FUZZING_HELP_PATH,
- type: REPORT_TYPE_COVERAGE_FUZZING,
- },
- {
- name: API_FUZZING_NAME,
- description: API_FUZZING_DESCRIPTION,
- helpPath: API_FUZZING_HELP_PATH,
- type: REPORT_TYPE_API_FUZZING,
- },
- {
- name: LICENSE_COMPLIANCE_NAME,
- description: LICENSE_COMPLIANCE_DESCRIPTION,
- helpPath: LICENSE_COMPLIANCE_HELP_PATH,
- type: REPORT_TYPE_LICENSE_COMPLIANCE,
- },
-];
-
export const securityFeatures = [
{
name: SAST_NAME,
@@ -219,7 +157,7 @@ export const securityFeatures = [
// This field will eventually come from the backend, the progress is
// tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
- canEnableByMergeRequest: window.gon.features?.secDependencyScanningUiEnable,
+ canEnableByMergeRequest: true,
},
{
name: CONTAINER_SCANNING_NAME,
@@ -262,6 +200,7 @@ export const securityFeatures = [
name: COVERAGE_FUZZING_NAME,
description: COVERAGE_FUZZING_DESCRIPTION,
helpPath: COVERAGE_FUZZING_HELP_PATH,
+ configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
},
];
@@ -300,3 +239,6 @@ export const featureToMutationMap = {
}),
},
};
+
+export const AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY =
+ 'security_configuration_auto_devops_enabled_dismissed_projects';
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 23cffde1f83..0ecfdf420db 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -83,7 +83,11 @@ export default {
<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">
+ <div
+ :class="statusClasses"
+ data-testid="feature-status"
+ :data-qa-selector="`${feature.type}_status`"
+ >
<template v-if="hasStatus">
<template v-if="enabled">
<gl-icon name="check-circle-filled" />
@@ -112,6 +116,7 @@ export default {
:href="feature.configurationPath"
variant="confirm"
:category="configurationButton.category"
+ :data-qa-selector="`${feature.type}_enable_button`"
class="gl-mt-5"
>
{{ configurationButton.text }}
@@ -125,7 +130,12 @@ export default {
class="gl-mt-5"
/>
- <gl-button v-else icon="external-link" :href="feature.configurationHelpPath" class="gl-mt-5">
+ <gl-button
+ v-else-if="feature.configurationHelpPath"
+ icon="external-link"
+ :href="feature.configurationHelpPath"
+ class="gl-mt-5"
+ >
{{ $options.i18n.configurationGuide }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/redesigned_app.vue b/app/assets/javascripts/security_configuration/components/redesigned_app.vue
deleted file mode 100644
index 915da378a4f..00000000000
--- a/app/assets/javascripts/security_configuration/components/redesigned_app.vue
+++ /dev/null
@@ -1,179 +0,0 @@
-<script>
-import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
-import FeatureCard from './feature_card.vue';
-import SectionLayout from './section_layout.vue';
-import UpgradeBanner from './upgrade_banner.vue';
-
-export const i18n = {
- compliance: s__('SecurityConfiguration|Compliance'),
- configurationHistory: s__('SecurityConfiguration|Configuration history'),
- securityTesting: s__('SecurityConfiguration|Security testing'),
- latestPipelineDescription: s__(
- `SecurityConfiguration|The status of the tools only applies to the
- default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.`,
- ),
- description: s__(
- `SecurityConfiguration|Once you've enabled a scan for the default branch,
- any subsequent feature branch you create will include the scan.`,
- ),
- securityConfiguration: __('Security Configuration'),
-};
-
-export default {
- i18n,
- components: {
- GlTab,
- GlLink,
- GlTabs,
- GlSprintf,
- FeatureCard,
- SectionLayout,
- UpgradeBanner,
- AutoDevOpsAlert,
- UserCalloutDismisser,
- },
- props: {
- augmentedSecurityFeatures: {
- type: Array,
- required: true,
- },
- augmentedComplianceFeatures: {
- type: Array,
- required: true,
- },
- gitlabCiPresent: {
- type: Boolean,
- required: false,
- default: false,
- },
- autoDevopsEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- canEnableAutoDevops: {
- type: Boolean,
- required: false,
- default: false,
- },
- gitlabCiHistoryPath: {
- type: String,
- required: false,
- default: '',
- },
- latestPipelinePath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- canUpgrade() {
- return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some(
- ({ available }) => !available,
- );
- },
- canViewCiHistory() {
- return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
- },
- shouldShowDevopsAlert() {
- return !this.autoDevopsEnabled && !this.gitlabCiPresent && this.canEnableAutoDevops;
- },
- },
-};
-</script>
-
-<template>
- <article>
- <user-callout-dismisser
- v-if="shouldShowDevopsAlert"
- feature-name="security_configuration_devops_alert"
- >
- <template #default="{ dismiss, shouldShowCallout }">
- <auto-dev-ops-alert v-if="shouldShowCallout" class="gl-mt-3" @dismiss="dismiss" />
- </template>
- </user-callout-dismisser>
- <header>
- <h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
- </header>
- <user-callout-dismisser v-if="canUpgrade" feature-name="security_configuration_upgrade_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <upgrade-banner v-if="shouldShowCallout" @close="dismiss" />
- </template>
- </user-callout-dismisser>
-
- <gl-tabs content-class="gl-pt-6">
- <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
- <section-layout :heading="$options.i18n.securityTesting">
- <template #description>
- <p>
- <span data-testid="latest-pipeline-info-security">
- <gl-sprintf
- v-if="latestPipelinePath"
- :message="$options.i18n.latestPipelineDescription"
- >
- <template #link="{ content }">
- <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
-
- {{ $options.i18n.description }}
- </p>
- <p v-if="canViewCiHistory">
- <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
- $options.i18n.configurationHistory
- }}</gl-link>
- </p>
- </template>
-
- <template #features>
- <feature-card
- v-for="feature in augmentedSecurityFeatures"
- :key="feature.type"
- data-testid="security-testing-card"
- :feature="feature"
- class="gl-mb-6"
- />
- </template>
- </section-layout>
- </gl-tab>
- <gl-tab data-testid="compliance-testing-tab" :title="$options.i18n.compliance">
- <section-layout :heading="$options.i18n.compliance">
- <template #description>
- <p>
- <span data-testid="latest-pipeline-info-compliance">
- <gl-sprintf
- v-if="latestPipelinePath"
- :message="$options.i18n.latestPipelineDescription"
- >
- <template #link="{ content }">
- <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
-
- {{ $options.i18n.description }}
- </p>
- <p v-if="canViewCiHistory">
- <gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{
- $options.i18n.configurationHistory
- }}</gl-link>
- </p>
- </template>
- <template #features>
- <feature-card
- v-for="feature in augmentedComplianceFeatures"
- :key="feature.type"
- :feature="feature"
- class="gl-mb-6"
- />
- </template>
- </section-layout>
- </gl-tab>
- </gl-tabs>
- </article>
-</template>
diff --git a/app/assets/javascripts/security_configuration/components/section_layout.vue b/app/assets/javascripts/security_configuration/components/section_layout.vue
index e351f9b9d8d..1fe8dd862a0 100644
--- a/app/assets/javascripts/security_configuration/components/section_layout.vue
+++ b/app/assets/javascripts/security_configuration/components/section_layout.vue
@@ -11,7 +11,7 @@ export default {
</script>
<template>
- <div class="row gl-line-height-20">
+ <div class="row gl-line-height-20 gl-pt-6">
<div class="col-lg-4">
<h2 class="gl-font-size-h2 gl-mt-0">{{ heading }}</h2>
<slot name="description"></slot>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue
deleted file mode 100644
index 2541c29224a..00000000000
--- a/app/assets/javascripts/security_configuration/components/upgrade.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { UPGRADE_CTA } from './constants';
-
-export default {
- components: {
- GlLink,
- GlSprintf,
- },
- inject: {
- upgradePath: {
- from: 'upgradePath',
- default: '#',
- },
- },
- i18n: {
- UPGRADE_CTA,
- },
-};
-</script>
-
-<template>
- <span>
- <gl-sprintf :message="$options.i18n.UPGRADE_CTA">
- <template #link="{ content }">
- <gl-link target="_blank" :href="upgradePath">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </span>
-</template>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
index ca0f9e5c85a..79e6b9d7a23 100644
--- a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
+++ b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
@@ -8,20 +8,18 @@ export default {
},
inject: ['upgradePath'],
i18n: {
- title: s__('SecurityConfiguration|Secure your project with Ultimate'),
+ title: s__('SecurityConfiguration|Secure your project'),
bodyStart: s__(
- `SecurityConfiguration|GitLab Ultimate checks your application for security vulnerabilities
- that may lead to unauthorized access, data leaks, and denial of service
- attacks. Its features include:`,
+ `SecurityConfiguration|Immediately begin risk analysis and remediation with application security features. Start with SAST and Secret Detection, available to all plans. Upgrade to Ultimate to get all features, including:`,
),
bodyListItems: [
- s__('SecurityConfiguration|Vulnerability details and statistics in the merge request.'),
- s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups.'),
- s__('SecurityConfiguration|Runtime security metrics for application environments.'),
+ s__('SecurityConfiguration|Vulnerability details and statistics in the merge request'),
+ s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups'),
+ s__('SecurityConfiguration|Runtime security metrics for application environments'),
+ s__(
+ 'SecurityConfiguration|More scan types, including Container Scanning, DAST, Dependency Scanning, Fuzzing, and Licence Compliance',
+ ),
],
- bodyEnd: s__(
- 'SecurityConfiguration|With the information provided, you can immediately begin risk analysis and remediation within GitLab.',
- ),
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
},
};
@@ -32,14 +30,14 @@ export default {
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
:button-link="upgradePath"
+ variant="introduction"
v-on="$listeners"
>
<p>{{ $options.i18n.bodyStart }}</p>
- <ul>
+ <ul class="gl-pl-6">
<li v-for="bodyListItem in $options.i18n.bodyListItems" :key="bodyListItem">
{{ bodyListItem }}
</li>
</ul>
- <p>{{ $options.i18n.bodyEnd }}</p>
</gl-banner>
</template>
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index f05bd79258e..60d2c0d4e5a 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -4,14 +4,17 @@ import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
-import RedesignedSecurityConfigurationApp from './components/redesigned_app.vue';
import { augmentFeatures } from './utils';
-export const initRedesignedSecurityConfiguration = (el) => {
+export const initSecurityConfiguration = (el) => {
+ if (!el) {
+ return null;
+ }
+
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
const {
@@ -40,7 +43,7 @@ export const initRedesignedSecurityConfiguration = (el) => {
autoDevopsPath,
},
render(createElement) {
- return createElement(RedesignedSecurityConfigurationApp, {
+ return createElement(SecurityConfigurationApp, {
props: {
augmentedComplianceFeatures,
augmentedSecurityFeatures,
@@ -56,33 +59,3 @@ export const initRedesignedSecurityConfiguration = (el) => {
},
});
};
-
-export const initCESecurityConfiguration = (el) => {
- if (!el) {
- return null;
- }
-
- if (gon.features?.securityConfigurationRedesign) {
- return initRedesignedSecurityConfiguration(el);
- }
-
- Vue.use(VueApollo);
-
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
- });
-
- const { projectPath, upgradePath } = el.dataset;
-
- return new Vue({
- el,
- apolloProvider,
- provide: {
- projectPath,
- upgradePath,
- },
- render(createElement) {
- return createElement(SecurityConfigurationApp);
- },
- });
-};
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index 4c1f0d892af..2f31d8ef3fb 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,8 +1,16 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle } from '@gitlab/ui';
+import {
+ GlFormGroup,
+ GlButton,
+ GlModal,
+ GlToast,
+ GlToggle,
+ GlLink,
+ GlSafeHtmlDirective,
+} from '@gitlab/ui';
import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
@@ -15,9 +23,13 @@ export default {
GlButton,
GlModal,
GlToggle,
+ GlLink,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
},
formLabels: {
- createProject: __('Create Project'),
+ createProject: __('Self monitoring'),
},
data() {
return {
@@ -48,7 +60,7 @@ export default {
if (this.projectCreated) {
return sprintf(
s__(
- 'SelfMonitoring|Enabling this feature creates a %{projectLinkStart}project%{projectLinkEnd} that can be used to monitor the health of your instance.',
+ 'SelfMonitoring|Self monitoring is active. Use the %{projectLinkStart}self monitoring project%{projectLinkEnd} to monitor the health of your instance.',
),
{
projectLinkStart: `<a href="${this.selfMonitorProjectFullUrl}">`,
@@ -59,9 +71,12 @@ export default {
}
return s__(
- 'SelfMonitoring|Enabling this feature creates a project that can be used to monitor the health of your instance.',
+ 'SelfMonitoring|Activate self monitoring to create a project to use to monitor the health of your instance.',
);
},
+ helpDocsPath() {
+ return helpPagePath('administration/monitoring/gitlab_self_monitoring_project/index');
+ },
},
watch: {
selfMonitorEnabled() {
@@ -126,12 +141,13 @@ export default {
</h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
- {{ s__('SelfMonitoring|Enable or disable instance self monitoring') }}
+ {{ s__('SelfMonitoring|Activate or deactivate instance self monitoring.') }}
+ <gl-link :href="helpDocsPath">{{ __('Learn more.') }}</gl-link>
</p>
</div>
<div class="settings-content">
<form name="self-monitoring-form">
- <p ref="selfMonitoringFormText" v-html="selfMonitoringFormText"></p>
+ <p ref="selfMonitoringFormText" v-safe-html="selfMonitoringFormText"></p>
<gl-form-group>
<gl-toggle
v-model="selfMonitorEnabled"
@@ -142,9 +158,9 @@ export default {
</form>
</div>
<gl-modal
- :title="s__('SelfMonitoring|Disable self monitoring?')"
+ :title="s__('SelfMonitoring|Deactivate self monitoring?')"
:modal-id="modalId"
- :ok-title="__('Delete project')"
+ :ok-title="__('Delete self monitoring project')"
:cancel-title="__('Cancel')"
ok-variant="danger"
category="primary"
@@ -154,7 +170,7 @@ export default {
<div>
{{
s__(
- 'SelfMonitoring|Disabling this feature will delete the self monitoring project. Are you sure you want to delete the project?',
+ 'SelfMonitoring|Deactivating self monitoring deletes the self monitoring project. Are you sure you want to deactivate self monitoring and delete the project?',
)
}}
</div>
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index 670ee547b02..f37b654b00a 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -56,7 +56,7 @@ export const requestCreateProjectSuccess = ({ commit, dispatch }, selfMonitorDat
commit(types.SET_LOADING, false);
commit(types.SET_PROJECT_URL, selfMonitorData.project_full_path);
commit(types.SET_ALERT_CONTENT, {
- message: s__('SelfMonitoring|Self monitoring project has been successfully created.'),
+ message: s__('SelfMonitoring|Self monitoring project successfully created.'),
actionText: __('View project'),
actionName: 'viewSelfMonitorProject',
});
@@ -108,7 +108,7 @@ export const requestDeleteProjectSuccess = ({ commit }) => {
commit(types.SET_PROJECT_URL, '');
commit(types.SET_PROJECT_CREATED, false);
commit(types.SET_ALERT_CONTENT, {
- message: s__('SelfMonitoring|Self monitoring project has been successfully deleted.'),
+ message: s__('SelfMonitoring|Self monitoring project successfully deleted.'),
actionText: __('Undo'),
actionName: 'createProject',
});
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
index 98fc0b0a783..2a237e7ace0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
import AssigneeAvatar from './assignee_avatar.vue';
@@ -32,10 +32,9 @@ const generateAssigneeTooltip = ({
}
if (tooltipHasName && statusInformation.length) {
- return sprintf(__('%{name} %{status}'), {
- name,
- status: statusInformation.map(paranthesize).join(' '),
- });
+ const status = statusInformation.map(paranthesize).join(' ');
+
+ return `${name} ${status}`;
}
return name;
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 4b3b22f6db3..d9c5edc91f1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -50,7 +50,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
v-if="editable"
- class="js-sidebar-dropdown-toggle edit-link float-right"
+ class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
data-track-event="click_edit_button"
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index b7832ca679c..55179947756 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -55,12 +55,13 @@ export default {
},
getUpdateVariables(dropdownLabels) {
const currentLabelIds = this.selectedLabels.map((label) => label.id);
- const userAddedLabelIds = dropdownLabels
- .filter((label) => label.set)
- .map((label) => label.id);
- const userRemovedLabelIds = dropdownLabels
- .filter((label) => !label.set)
- .map((label) => label.id);
+ const dropdownLabelIds = dropdownLabels.map((label) => label.id);
+ const userAddedLabelIds = this.glFeatures.labelsWidget
+ ? difference(dropdownLabelIds, currentLabelIds)
+ : dropdownLabels.filter((label) => label.set).map((label) => label.id);
+ const userRemovedLabelIds = this.glFeatures.labelsWidget
+ ? difference(currentLabelIds, dropdownLabelIds)
+ : dropdownLabels.filter((label) => !label.set).map((label) => label.id);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
@@ -155,7 +156,7 @@ export default {
:labels-manage-path="labelsManagePath"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
- :variant="$options.sidebar"
+ :variant="$options.variant"
data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 81ee0a73739..19543d0927a 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -90,7 +90,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<a
v-if="isEditable"
- class="float-right lock-edit"
+ class="float-right lock-edit btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary gl-mr-n2"
href="#"
data-testid="edit-link"
data-track-event="click_edit_button"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 650aa603f18..ad4bfe5b665 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -33,6 +33,11 @@ export default {
required: false,
default: true,
},
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -95,7 +100,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
- <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2">
+ <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2 gl-line-height-20">
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
{{ participantLabel }}
</div>
@@ -107,10 +112,11 @@ export default {
>
<a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image
- :lazy="true"
+ :lazy="lazy"
:img-src="participant.avatar_url || participant.avatarUrl"
:size="24"
:tooltip-text="participant.name"
+ :img-alt="participant.name"
css-classes="avatar-inline"
tooltip-placement="bottom"
/>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index 9927a0f9114..39f72b251c7 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -64,6 +64,7 @@ export default {
:loading="isLoading"
:participants="participants"
:number-of-less-participants="7"
+ :lazy="false"
class="block participants"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 295027186cc..1243603805a 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -38,7 +38,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
v-if="editable"
- class="js-sidebar-dropdown-toggle edit-link float-right"
+ class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index fdf63c23552..5dc93476120 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -23,6 +23,7 @@ export default {
GlLink,
SeverityToken,
},
+ inject: ['canUpdate'],
props: {
projectPath: {
type: String,
@@ -153,6 +154,7 @@ export default {
>
{{ $options.i18n.SEVERITY }}
<gl-link
+ v-if="canUpdate"
data-testid="editButton"
href="#"
@click="toggleFormDropdown"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 2e00a23de7c..8ccc0102c3d 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -13,6 +13,7 @@ import {
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
+import { timeFor } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
@@ -22,6 +23,7 @@ import {
issuableAttributesQueries,
noAttributeId,
defaultEpicSort,
+ epicIidPattern,
} from '~/sidebar/constants';
export default {
@@ -118,17 +120,37 @@ export default {
return query;
},
skip() {
+ if (this.isEpic && this.searchTerm.startsWith('&') && this.searchTerm.length < 2) {
+ return true;
+ }
+
return !this.editing;
},
debounce: 250,
variables() {
- return {
+ if (!this.isEpic) {
+ return {
+ fullPath: this.attrWorkspacePath,
+ title: this.searchTerm,
+ state: this.$options.IssuableAttributeState[this.issuableAttribute],
+ };
+ }
+
+ const variables = {
fullPath: this.attrWorkspacePath,
- title: this.searchTerm,
- in: this.searchTerm && this.issuableAttribute === IssuableType.Epic ? 'TITLE' : undefined,
state: this.$options.IssuableAttributeState[this.issuableAttribute],
- sort: this.issuableAttribute === IssuableType.Epic ? defaultEpicSort : null,
+ sort: defaultEpicSort,
};
+
+ if (epicIidPattern.test(this.searchTerm)) {
+ const matches = this.searchTerm.match(epicIidPattern);
+ variables.iidStartsWith = matches.groups.iid;
+ } else if (this.searchTerm !== '') {
+ variables.in = 'TITLE';
+ variables.title = this.searchTerm;
+ }
+
+ return variables;
},
update(data) {
if (data?.workspace) {
@@ -183,6 +205,9 @@ export default {
attributeTypeIcon() {
return this.icon || this.issuableAttribute;
},
+ tooltipText() {
+ return timeFor(this.currentAttribute?.dueDate);
+ },
i18n() {
return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
@@ -214,6 +239,9 @@ export default {
),
};
},
+ isEpic() {
+ return this.issuableAttribute === IssuableType.Epic;
+ },
},
methods: {
updateAttribute(attributeId) {
@@ -322,6 +350,7 @@ export default {
:currentAttribute="currentAttribute"
>
<gl-link
+ v-gl-tooltip="tooltipText"
class="gl-text-gray-900! gl-font-weight-bold"
:href="attributeUrl"
:data-qa-selector="`${issuableAttribute}_link`"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 7c496cc422a..89aa03fd954 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -132,8 +132,9 @@ export default {
<slot name="collapsed-right"></slot>
<gl-button
v-if="canUpdate && !initialLoading && canEdit"
- variant="link"
- class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
+ category="tertiary"
+ size="small"
+ class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
data-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index d1a5685fdd3..7c157fe2775 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '../../../locale';
@@ -9,15 +8,16 @@ export default {
components: {
GlButton,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
computed: {
href() {
return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
},
estimateText() {
return sprintf(
- s__(
- 'estimateCommand|%{slash_command} will update the estimated time with the latest command.',
- ),
+ s__('estimateCommand|%{slash_command} overwrites the total estimated time.'),
{
slash_command: '<code>/estimate</code>',
},
@@ -26,7 +26,7 @@ export default {
},
spendText() {
return sprintf(
- s__('spendCommand|%{slash_command} will update the sum of the time spent.'),
+ s__('spendCommand|%{slash_command} adds or subtracts time already spent.'),
{
slash_command: '<code>/spend</code>',
},
@@ -41,9 +41,9 @@ export default {
<div data-testid="helpPane" class="time-tracking-help-state">
<div class="time-tracking-info">
<h4>{{ __('Track time with quick actions') }}</h4>
- <p>{{ __('Quick actions can be used in the issues description and comment boxes.') }}</p>
- <p v-html="estimateText"></p>
- <p v-html="spendText"></p>
+ <p>{{ __('Quick actions can be used in description and comment boxes.') }}</p>
+ <p v-safe-html="estimateText"></p>
+ <p v-safe-html="spendText"></p>
<gl-button :href="href">{{ __('Learn more') }}</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 8a14998910b..d4a8abb81a8 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -62,8 +62,8 @@ export default {
formatDate(date) {
return formatDate(date, TIME_DATE_FORMAT);
},
- getNote(note) {
- return note?.body;
+ getSummary(summary, note) {
+ return summary ?? note?.body;
},
getTotalTimeSpent() {
const seconds = this.report.reduce((acc, item) => acc + item.timeSpent, 0);
@@ -81,7 +81,7 @@ export default {
{ 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 },
+ { key: 'summary', label: __('Summary / Note'), sortable: true },
],
};
</script>
@@ -107,8 +107,8 @@ export default {
<div>{{ getTotalTimeSpent() }}</div>
</template>
- <template #cell(note)="{ item: { note } }">
- <div>{{ getNote(note) }}</div>
+ <template #cell(summary)="{ item: { summary, note } }">
+ <div>{{ getSummary(summary, note) }}</div>
</template>
<template #foot(note)>&nbsp;</template>
</gl-table>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 08ee4379c0c..fd43fb80b7f 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -48,6 +48,8 @@ export const ASSIGNEES_DEBOUNCE_DELAY = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const defaultEpicSort = 'TITLE_ASC';
+export const epicIidPattern = /^&(?<iid>\d+)$/;
+
export const assigneesQueries = {
[IssuableType.Issue]: {
query: getIssueAssignees,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index dd1b439c482..031472a7d20 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -24,6 +24,7 @@ import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
+import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
@@ -256,6 +257,7 @@ export function mountSidebarLabels() {
allowLabelEdit: parseBoolean(el.dataset.canEdit),
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
+ variant: DropdownVariant.Sidebar,
},
render: (createElement) => createElement(SidebarLabels),
});
@@ -493,7 +495,7 @@ function mountSeverityComponent() {
return false;
}
- const { fullPath, iid, severity } = getSidebarOptions();
+ const { fullPath, iid, severity, editable } = getSidebarOptions();
return new Vue({
el: severityContainerEl,
@@ -501,6 +503,9 @@ function mountSeverityComponent() {
components: {
SidebarSeverity,
},
+ provide: {
+ canUpdate: editable,
+ },
render: (createElement) =>
createElement('sidebar-severity', {
props: {
diff --git a/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql
new file mode 100644
index 00000000000..dceab61ed26
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql
@@ -0,0 +1,20 @@
+#import "./milestone.fragment.graphql"
+
+query groupMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ id
+ attributes: milestones(
+ searchTitle: $title
+ state: $state
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ first: 20
+ includeAncestors: true
+ ) {
+ nodes {
+ ...MilestoneFragment
+ state
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
index 2ffd58a2da1..d4f7e703692 100644
--- a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
+++ b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
@@ -2,5 +2,6 @@ fragment MilestoneFragment on Milestone {
id
title
webUrl: webPath
+ dueDate
expired
}
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 27b3a30b40a..8481ac2b9c9 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -29,15 +29,6 @@ export default {
update(data) {
return this.onContentUpdate(data);
},
- result() {
- if (this.activeViewerType === RICH_BLOB_VIEWER) {
- // eslint-disable-next-line vue/no-mutating-props
- this.blob.richViewer.renderError = null;
- } else {
- // eslint-disable-next-line vue/no-mutating-props
- this.blob.simpleViewer.renderError = null;
- }
- },
skip() {
return this.viewer.renderError;
},
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index a8f95748e7e..466b273cae4 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -54,6 +54,7 @@ export default {
},
},
},
+ inject: ['reportAbusePath'],
props: {
snippet: {
type: Object,
@@ -93,7 +94,6 @@ export default {
click: this.showDeleteModal,
variant: 'danger',
category: 'secondary',
- cssClass: 'ml-2',
},
{
condition: this.canCreateSnippet,
@@ -103,10 +103,18 @@ export default {
: joinPaths('/', gon.relative_url_root, '/-/snippets/new'),
variant: 'success',
category: 'secondary',
- cssClass: 'ml-2',
+ },
+ {
+ condition: this.reportAbusePath,
+ text: __('Submit as spam'),
+ href: this.reportAbusePath,
+ title: __('Submit as spam'),
},
];
},
+ hasPersonalSnippetActions() {
+ return Boolean(this.personalSnippetActions.filter(({ condition }) => condition).length);
+ },
editLink() {
return `${this.snippet.webUrl}/edit`;
},
@@ -212,7 +220,7 @@ export default {
</div>
</div>
- <div class="detail-page-header-actions">
+ <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
<div
@@ -221,6 +229,7 @@ export default {
v-gl-tooltip
:title="action.title"
class="d-inline-block"
+ :class="{ 'gl-ml-3': index > 0 }"
>
<gl-button
:disabled="action.disabled"
@@ -239,15 +248,17 @@ export default {
</div>
<div class="d-block d-sm-none dropdown">
<gl-dropdown :text="__('Options')" block>
- <gl-dropdown-item
- v-for="(action, index) in personalSnippetActions"
- :key="index"
- :disabled="action.disabled"
- :title="action.title"
- :href="action.href"
- @click="action.click ? action.click() : undefined"
- >{{ action.text }}</gl-dropdown-item
- >
+ <template v-for="(action, index) in personalSnippetActions">
+ <gl-dropdown-item
+ v-if="action.condition"
+ :key="index"
+ :disabled="action.disabled"
+ :title="action.title"
+ :href="action.href"
+ @click="action.click ? action.click() : undefined"
+ >{{ action.text }}</gl-dropdown-item
+ >
+ </template>
</gl-dropdown>
</div>
</div>
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index 789332ce5b7..dec8dcec179 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -14,13 +14,20 @@ export default function appFactory(el, Component) {
}
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { batchMax: 1 }),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ batchMax: 1,
+ assumeImmutableResults: true,
+ },
+ ),
});
const {
visibilityLevels = '[]',
selectedLevel,
multipleLevelsRestricted,
+ reportAbusePath,
...restDataset
} = el.dataset;
@@ -31,6 +38,7 @@ export default function appFactory(el, Component) {
visibilityLevels: JSON.parse(visibilityLevels),
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
+ reportAbusePath,
},
render(createElement) {
return createElement(Component, {
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 7552eae97fc..b72befef56b 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -1,3 +1,4 @@
+import { isEmpty } from 'lodash';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
const blobsDefault = [];
@@ -12,20 +13,18 @@ export const getSnippetMixin = {
};
},
update(data) {
- const res = data.snippets.nodes[0];
+ const res = { ...data.snippets.nodes[0] };
// Set `snippet.blobs` since some child components are coupled to this.
- if (res) {
+ if (!isEmpty(res)) {
// It's possible for us to not get any blobs in a response.
// In this case, we should default to current blobs.
- res.blobs = res.blobs ? res.blobs.nodes : this.blobs;
+ res.blobs = res.blobs ? res.blobs.nodes : blobsDefault;
+ res.description = res.description || '';
}
return res;
},
- result(res) {
- this.blobs = res.data.snippets.nodes[0]?.blobs || blobsDefault;
- },
skip() {
return this.newSnippet;
},
@@ -41,12 +40,14 @@ export const getSnippetMixin = {
return {
snippet: {},
newSnippet: !this.snippetGid,
- blobs: blobsDefault,
};
},
computed: {
isLoading() {
return this.$apollo.queries.snippet.loading;
},
+ blobs() {
+ return this.snippet?.blobs || [];
+ },
},
};
diff --git a/app/assets/javascripts/sourcegraph/load.js b/app/assets/javascripts/sourcegraph/load.js
index f9491505d42..f41efc10d6c 100644
--- a/app/assets/javascripts/sourcegraph/load.js
+++ b/app/assets/javascripts/sourcegraph/load.js
@@ -1,6 +1,3 @@
import initSourcegraph from './index';
-/**
- * Load sourcegraph in it's own listener so that it's isolated from failures.
- */
-document.addEventListener('DOMContentLoaded', initSourcegraph);
+initSourcegraph();
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 474b5132bc6..96b6e78c668 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,7 +1,5 @@
/* eslint-disable consistent-return */
-import $ from 'jquery';
-
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
@@ -12,14 +10,30 @@ import $ from 'jquery';
// <div class="js-syntax-highlight"></div>
//
-export default function syntaxHighlight(el) {
- if ($(el).hasClass('js-syntax-highlight')) {
- // Given the element itself, apply highlighting
- return $(el).addClass(gon.user_color_scheme);
- }
- // Given a parent element, recurse to any of its applicable children
- const $children = $(el).find('.js-syntax-highlight');
- if ($children.length) {
- return syntaxHighlight($children);
+export default function syntaxHighlight($els = null) {
+ if (!$els) return;
+
+ const els = $els.get ? $els.get() : $els;
+ const handler = (el) => {
+ if (el.classList.contains('js-syntax-highlight')) {
+ // Given the element itself, apply highlighting
+ return el.classList.add(gon.user_color_scheme);
+ }
+ // Given a parent element, recurse to any of its applicable children
+ const children = el.querySelectorAll('.js-syntax-highlight');
+ if (children.length) {
+ return syntaxHighlight(children);
+ }
+ };
+
+ // In order to account for NodeList returned by document.querySelectorAll,
+ // we should rather check whether the els object is iterable
+ // instead of relying on Array.isArray()
+ const isIterable = typeof els[Symbol.iterator] === 'function';
+
+ if (isIterable) {
+ els.forEach((el) => handler(el));
+ } else {
+ handler(els);
}
}
diff --git a/app/assets/javascripts/terraform/components/empty_state.vue b/app/assets/javascripts/terraform/components/empty_state.vue
index d86ba3af2b1..a5a613b7282 100644
--- a/app/assets/javascripts/terraform/components/empty_state.vue
+++ b/app/assets/javascripts/terraform/components/empty_state.vue
@@ -1,12 +1,12 @@
<script>
-import { GlEmptyState, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlIcon, GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: {
GlEmptyState,
GlIcon,
GlLink,
- GlSprintf,
},
props: {
image: {
@@ -14,6 +14,11 @@ export default {
required: true,
},
},
+ computed: {
+ docsUrl() {
+ return helpPagePath('user/infrastructure/terraform_state');
+ },
+ },
};
</script>
@@ -21,23 +26,10 @@ export default {
<gl-empty-state :svg-path="image" :title="s__('Terraform|Get started with Terraform')">
<template #description>
<p>
- <gl-sprintf
- :message="
- s__(
- 'Terraform|Find out how to use the %{linkStart}GitLab managed Terraform State%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link
- href="https://docs.gitlab.com/ee/user/infrastructure/index.html"
- target="_blank"
- >
- {{ content }}
- <gl-icon name="external-link" />
- </gl-link>
- </template>
- </gl-sprintf>
+ <gl-link :href="docsUrl" target="_blank"
+ >{{ s__('Terraform|How to use GitLab-managed Terraform State?') }}
+ <gl-icon name="external-link"
+ /></gl-link>
</p>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
new file mode 100644
index 00000000000..2cb10d4ae23
--- /dev/null
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+export default {
+ i18n: {
+ title: s__('Terraform|Terraform init command'),
+ explanatoryText: s__(
+ `Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}.`,
+ ),
+ closeText: __('Close'),
+ copyToClipboardText: __('Copy'),
+ },
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ ModalCopyButton,
+ },
+ inject: ['accessTokensPath', 'terraformApiUrl', 'username'],
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ stateName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ closeModalProps() {
+ return {
+ text: this.$options.i18n.closeText,
+ attributes: [],
+ };
+ },
+ },
+ methods: {
+ getModalInfoCopyStr() {
+ return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+terraform init \\
+ -backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="username=${this.username}" \\
+ -backend-config="password=$GITLAB_ACCESS_TOKEN" \\
+ -backend-config="lock_method=POST" \\
+ -backend-config="unlock_method=DELETE" \\
+ -backend-config="retry_wait_min=5"
+ `;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="initCommandModal"
+ :modal-id="modalId"
+ :title="$options.i18n.title"
+ :action-cancel="closeModalProps"
+ >
+ <p data-testid="init-command-explanatory-text">
+ <gl-sprintf :message="$options.i18n.explanatoryText">
+ <template #link="{ content }">
+ <gl-link :href="accessTokensPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div class="gl-display-flex">
+ <pre class="gl-bg-gray gl-white-space-pre-wrap" data-testid="terraform-init-command">{{
+ getModalInfoCopyStr()
+ }}</pre>
+ <modal-copy-button
+ :title="$options.i18n.copyToClipboardText"
+ :text="getModalInfoCopyStr()"
+ :modal-id="$options.modalId"
+ data-testid="init-command-copy-clipboard"
+ css-classes="gl-align-self-start gl-ml-2"
+ />
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue
index c4fd97188de..817c421823c 100644
--- a/app/assets/javascripts/terraform/components/states_table_actions.vue
+++ b/app/assets/javascripts/terraform/components/states_table_actions.vue
@@ -8,12 +8,14 @@ import {
GlIcon,
GlModal,
GlSprintf,
+ GlModalDirective,
} from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
+import InitCommandModal from './init_command_modal.vue';
export default {
components: {
@@ -25,6 +27,10 @@ export default {
GlIcon,
GlModal,
GlSprintf,
+ InitCommandModal,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
state: {
@@ -36,6 +42,7 @@ export default {
return {
showRemoveModal: false,
removeConfirmText: '',
+ showCommandModal: false,
};
},
i18n: {
@@ -43,7 +50,7 @@ export default {
errorUpdate: s__('Terraform|An error occurred while changing the state file'),
lock: s__('Terraform|Lock'),
modalBody: s__(
- 'Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, only the state file with all its versions are to be removed. This action is non-revertible.',
+ 'Terraform|You are about to remove the state file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, and only the state file with all its versions will be removed. This action cannot be undone.',
),
modalCancel: s__('Terraform|Cancel'),
modalHeader: s__('Terraform|Are you sure you want to remove the Terraform State %{name}?'),
@@ -54,6 +61,7 @@ export default {
remove: s__('Terraform|Remove state file and versions'),
removeSuccessful: s__('Terraform|%{name} successfully removed'),
unlock: s__('Terraform|Unlock'),
+ copyCommand: s__('Terraform|Copy Terraform init command'),
},
computed: {
cancelModalProps() {
@@ -74,6 +82,9 @@ export default {
attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }],
};
},
+ commandModalId() {
+ return `init-command-modal-${this.state.name}`;
+ },
},
methods: {
hideModal() {
@@ -164,6 +175,9 @@ export default {
});
});
},
+ copyInitCommand() {
+ this.showCommandModal = true;
+ },
},
};
</script>
@@ -182,6 +196,14 @@ export default {
</template>
<gl-dropdown-item
+ v-gl-modal-directive="commandModalId"
+ data-testid="terraform-state-copy-init-command"
+ @click="copyInitCommand"
+ >
+ {{ $options.i18n.copyCommand }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
v-if="state.latestVersion"
data-testid="terraform-state-download"
:download="`${state.name}.json`"
@@ -248,5 +270,11 @@ export default {
/>
</gl-form-group>
</gl-modal>
+
+ <init-command-modal
+ v-if="showCommandModal"
+ :modal-id="commandModalId"
+ :state-name="state.name"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 3f986423836..1b8cab0d51e 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -24,11 +24,16 @@ export default () => {
},
});
- const { emptyStateImage, projectPath } = el.dataset;
+ const { emptyStateImage, projectPath, accessTokensPath, terraformApiUrl, username } = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
+ provide: {
+ accessTokensPath,
+ terraformApiUrl,
+ username,
+ },
render(createElement) {
return createElement(TerraformList, {
props: {
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
index 24565c441d8..e739ec37739 100644
--- a/app/assets/javascripts/token_access/components/token_access.vue
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -187,12 +187,7 @@ export default {
/>
</template>
<template #footer>
- <gl-button
- variant="confirm"
- :disabled="isProjectPathEmpty"
- data-testid="add-project-button"
- @click="addProject"
- >
+ <gl-button variant="confirm" :disabled="isProjectPathEmpty" @click="addProject">
{{ $options.i18n.addProject }}
</gl-button>
<gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
index 777eda1c4d7..b6c9330c754 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -73,7 +73,6 @@ export default {
variant="danger"
icon="remove"
:aria-label="__('Remove access')"
- data-testid="remove-project-button"
@click="removeProject(item.fullPath)"
/>
</template>
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index cd0af59e4fe..598111e4086 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -1 +1,26 @@
export const SNOWPLOW_JS_SOURCE = 'gitlab-javascript';
+
+export const DEFAULT_SNOWPLOW_OPTIONS = {
+ namespace: 'gl',
+ hostname: window.location.hostname,
+ cookieDomain: window.location.hostname,
+ appId: '',
+ userFingerprint: false,
+ respectDoNotTrack: true,
+ forceSecureTracker: true,
+ eventMethod: 'post',
+ contexts: { webPage: true, performanceTiming: true },
+ formTracking: false,
+ linkClickTracking: false,
+ pageUnloadTimer: 10,
+ formTrackingConfig: {
+ forms: { allow: [] },
+ fields: { allow: [] },
+ },
+};
+
+export const ACTION_ATTR_SELECTOR = '[data-track-action]';
+export const LOAD_ACTION_ATTR_SELECTOR = '[data-track-action="render"]';
+
+export const DEPRECATED_EVENT_ATTR_SELECTOR = '[data-track-event]';
+export const DEPRECATED_LOAD_EVENT_ATTR_SELECTOR = '[data-track-event="render"]';
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
new file mode 100644
index 00000000000..bc9d7384ea4
--- /dev/null
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -0,0 +1,23 @@
+import getStandardContext from './get_standard_context';
+
+export function dispatchSnowplowEvent(
+ category = document.body.dataset.page,
+ action = 'generic',
+ data = {},
+) {
+ if (!category) {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ throw new Error('Tracking: no category provided for tracking.');
+ }
+
+ const { label, property, value, extra = {} } = data;
+
+ const standardContext = getStandardContext({ extra });
+ const contexts = [standardContext];
+
+ if (data.context) {
+ contexts.push(data.context);
+ }
+
+ return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
+}
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index 3714cac3fba..5417e2d969b 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -1,239 +1,20 @@
-import { omitBy, isUndefined } from 'lodash';
-import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
-import { getExperimentData } from '~/experimentation/utils';
+import { DEFAULT_SNOWPLOW_OPTIONS } from './constants';
import getStandardContext from './get_standard_context';
+import Tracking from './tracking';
-const DEFAULT_SNOWPLOW_OPTIONS = {
- namespace: 'gl',
- hostname: window.location.hostname,
- cookieDomain: window.location.hostname,
- appId: '',
- userFingerprint: false,
- respectDoNotTrack: true,
- forceSecureTracker: true,
- eventMethod: 'post',
- contexts: { webPage: true, performanceTiming: true },
- formTracking: false,
- linkClickTracking: false,
- pageUnloadTimer: 10,
- formTrackingConfig: {
- forms: { allow: [] },
- fields: { allow: [] },
- },
-};
-
-const addExperimentContext = (opts) => {
- const { experiment, ...options } = opts;
- if (experiment) {
- const data = getExperimentData(experiment);
- if (data) {
- const context = { schema: TRACKING_CONTEXT_SCHEMA, data };
- return { ...options, context };
- }
- }
- return options;
-};
-
-const renameKey = (o, oldKey, newKey) => {
- const ret = {};
- delete Object.assign(ret, o, { [newKey]: o[oldKey] })[oldKey];
- return ret;
-};
-
-const createEventPayload = (el, { suffix = '' } = {}) => {
- const {
- trackAction,
- trackEvent,
- trackValue,
- trackExtra,
- trackExperiment,
- trackContext,
- trackLabel,
- trackProperty,
- } = el?.dataset || {};
-
- const action = (trackAction || trackEvent) + (suffix || '');
- let value = trackValue || el.value || undefined;
- if (el.type === 'checkbox' && !el.checked) value = 0;
-
- let extra = trackExtra;
-
- if (extra !== undefined) {
- try {
- extra = JSON.parse(extra);
- } catch (e) {
- extra = undefined;
- }
- }
-
- const context = addExperimentContext({
- experiment: trackExperiment,
- context: trackContext,
- });
-
- const data = {
- label: trackLabel,
- property: trackProperty,
- value,
- extra,
- ...context,
- };
-
- return {
- action,
- data: omitBy(data, isUndefined),
- };
-};
-
-const eventHandler = (e, func, opts = {}) => {
- const el = e.target.closest('[data-track-event], [data-track-action]');
-
- if (!el) return;
-
- const { action, data } = createEventPayload(el, opts);
- func(opts.category, action, data);
-};
-
-const eventHandlers = (category, func) => {
- const handler = (opts) => (e) => eventHandler(e, func, { ...{ category }, ...opts });
- const handlers = [];
- handlers.push({ name: 'click', func: handler() });
- handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
- handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
- return handlers;
-};
-
-const dispatchEvent = (category = document.body.dataset.page, action = 'generic', data = {}) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- if (!category) throw new Error('Tracking: no category provided for tracking.');
-
- const { label, property, value, extra = {} } = data;
-
- const standardContext = getStandardContext({ extra });
- const contexts = [standardContext];
-
- if (data.context) {
- contexts.push(data.context);
- }
-
- return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
-};
-
-export default class Tracking {
- static queuedEvents = [];
- static initialized = false;
-
- static trackable() {
- return !['1', 'yes'].includes(
- window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
- );
- }
-
- static flushPendingEvents() {
- this.initialized = true;
-
- while (this.queuedEvents.length) {
- dispatchEvent(...this.queuedEvents.shift());
- }
- }
-
- static enabled() {
- return typeof window.snowplow === 'function' && this.trackable();
- }
-
- static event(...eventData) {
- if (!this.enabled()) return false;
-
- if (!this.initialized) {
- this.queuedEvents.push(eventData);
- return false;
- }
-
- return dispatchEvent(...eventData);
- }
-
- static bindDocument(category = document.body.dataset.page, parent = document) {
- if (!this.enabled() || parent.trackingBound) return [];
-
- // eslint-disable-next-line no-param-reassign
- parent.trackingBound = true;
-
- const handlers = eventHandlers(category, (...args) => this.event(...args));
- handlers.forEach((event) => parent.addEventListener(event.name, event.func));
- return handlers;
- }
-
- static trackLoadEvents(category = document.body.dataset.page, parent = document) {
- if (!this.enabled()) return [];
-
- const loadEvents = parent.querySelectorAll(
- '[data-track-action="render"], [data-track-event="render"]',
- );
-
- loadEvents.forEach((element) => {
- const { action, data } = createEventPayload(element);
- this.event(category, action, data);
- });
-
- return loadEvents;
- }
-
- static enableFormTracking(config, contexts = []) {
- if (!this.enabled()) return;
-
- if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Unable to enable form event tracking without allow rules.');
- }
-
- // Ignore default/standard schema
- const standardContext = getStandardContext();
- const userProvidedContexts = contexts.filter(
- (context) => context.schema !== standardContext.schema,
- );
-
- const mappedConfig = {};
- if (config.forms) mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
- if (config.fields) mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
-
- const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
-
- if (document.readyState === 'complete') enabler();
- else {
- document.addEventListener('readystatechange', () => {
- if (document.readyState === 'complete') enabler();
- });
- }
- }
-
- static mixin(opts = {}) {
- return {
- computed: {
- trackingCategory() {
- const localCategory = this.tracking ? this.tracking.category : null;
- return localCategory || opts.category;
- },
- trackingOptions() {
- const options = addExperimentContext(opts);
- return { ...options, ...this.tracking };
- },
- },
- methods: {
- track(action, data = {}) {
- const category = data.category || this.trackingCategory;
- const options = {
- ...this.trackingOptions,
- ...data,
- };
- Tracking.event(category, action, options);
- },
- },
- };
- }
-}
+export { Tracking as default };
+/**
+ * Tracker initialization as defined in:
+ * https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/javascript-tracker/javascript-tracker-v2/tracker-setup/initializing-a-tracker-2/.
+ * It also dispatches any event emitted before its execution.
+ *
+ * @returns {undefined}
+ */
export function initUserTracking() {
- if (!Tracking.enabled()) return;
+ if (!Tracking.enabled()) {
+ return;
+ }
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
@@ -242,8 +23,18 @@ export function initUserTracking() {
Tracking.flushPendingEvents();
}
+/**
+ * Enables tracking of built-in events: page views, page pings.
+ * Optionally enables form and link tracking (automatically).
+ * Attaches event handlers for data-attributes powered events, and
+ * load-events (on render).
+ *
+ * @returns {undefined}
+ */
export function initDefaultTrackers() {
- if (!Tracking.enabled()) return;
+ if (!Tracking.enabled()) {
+ return;
+ }
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
@@ -252,8 +43,13 @@ export function initDefaultTrackers() {
const standardContext = getStandardContext();
window.snowplow('trackPageView', null, [standardContext]);
- if (window.snowplowOptions.formTracking) Tracking.enableFormTracking(opts.formTrackingConfig);
- if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');
+ if (window.snowplowOptions.formTracking) {
+ Tracking.enableFormTracking(opts.formTrackingConfig);
+ }
+
+ if (window.snowplowOptions.linkClickTracking) {
+ window.snowplow('enableLinkClickTracking');
+ }
Tracking.bindDocument();
Tracking.trackLoadEvents();
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
new file mode 100644
index 00000000000..a1f745bc172
--- /dev/null
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -0,0 +1,193 @@
+import { LOAD_ACTION_ATTR_SELECTOR, DEPRECATED_LOAD_EVENT_ATTR_SELECTOR } from './constants';
+import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
+import getStandardContext from './get_standard_context';
+import { getEventHandlers, createEventPayload, renameKey, addExperimentContext } from './utils';
+
+export default class Tracking {
+ static queuedEvents = [];
+ static initialized = false;
+
+ /**
+ * (Legacy) Determines if tracking is enabled at the user level.
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT.
+ *
+ * @returns {Boolean}
+ */
+ static trackable() {
+ return !['1', 'yes'].includes(
+ window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack,
+ );
+ }
+
+ /**
+ * Determines if Snowplow is available/enabled.
+ *
+ * @returns {Boolean}
+ */
+ static enabled() {
+ return typeof window.snowplow === 'function' && this.trackable();
+ }
+
+ /**
+ * Dispatches a structured event per our taxonomy:
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
+ *
+ * If the library is not initialized and events are trying to be
+ * dispatched (data-attributes, load-events), they will be added
+ * to a queue to be flushed afterwards.
+ *
+ * @param {...any} eventData defined event taxonomy
+ * @returns {undefined|Boolean}
+ */
+ static event(...eventData) {
+ if (!this.enabled()) {
+ return false;
+ }
+
+ if (!this.initialized) {
+ this.queuedEvents.push(eventData);
+ return false;
+ }
+
+ return dispatchSnowplowEvent(...eventData);
+ }
+
+ /**
+ * Dispatches any event emitted before initialization.
+ *
+ * @returns {undefined}
+ */
+ static flushPendingEvents() {
+ this.initialized = true;
+
+ while (this.queuedEvents.length) {
+ dispatchSnowplowEvent(...this.queuedEvents.shift());
+ }
+ }
+
+ /**
+ * Attaches event handlers for data-attributes powered events.
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing data-attributes
+ * @returns {Array}
+ */
+ static bindDocument(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled() || parent.trackingBound) {
+ return [];
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ parent.trackingBound = true;
+
+ const handlers = getEventHandlers(category, (...args) => this.event(...args));
+ handlers.forEach((event) => parent.addEventListener(event.name, event.func));
+
+ return handlers;
+ }
+
+ /**
+ * Attaches event handlers for load-events (on render).
+ *
+ * @param {String} category - the default category for all events
+ * @param {HTMLElement} parent - element containing event targets
+ * @returns {Array}
+ */
+ static trackLoadEvents(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled()) {
+ return [];
+ }
+
+ const loadEvents = parent.querySelectorAll(
+ `${LOAD_ACTION_ATTR_SELECTOR}, ${DEPRECATED_LOAD_EVENT_ATTR_SELECTOR}`,
+ );
+
+ loadEvents.forEach((element) => {
+ const { action, data } = createEventPayload(element);
+ this.event(category, action, data);
+ });
+
+ return loadEvents;
+ }
+
+ /**
+ * Enable Snowplow automatic form tracking.
+ * The config param requires at least one array of either forms
+ * class names, or field name attributes.
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#form-tracking.
+ *
+ * @param {Object} config
+ * @param {Array} contexts
+ * @returns {undefined}
+ */
+ static enableFormTracking(config, contexts = []) {
+ if (!this.enabled()) {
+ return;
+ }
+
+ if (!Array.isArray(config?.forms?.allow) && !Array.isArray(config?.fields?.allow)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unable to enable form event tracking without allow rules.');
+ }
+
+ // Ignore default/standard schema
+ const standardContext = getStandardContext();
+ const userProvidedContexts = contexts.filter(
+ (context) => context.schema !== standardContext.schema,
+ );
+
+ const mappedConfig = {};
+ if (config.forms) {
+ mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
+ }
+
+ if (config.fields) {
+ mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
+ }
+
+ const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
+
+ if (document.readyState === 'complete') {
+ enabler();
+ } else {
+ document.addEventListener('readystatechange', () => {
+ if (document.readyState === 'complete') {
+ enabler();
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns an implementation of this class in the form of
+ * a Vue mixin.
+ *
+ * @param {Object} opts - default options for all events
+ * @returns {Object}
+ */
+ static mixin(opts = {}) {
+ return {
+ computed: {
+ trackingCategory() {
+ const localCategory = this.tracking ? this.tracking.category : null;
+ return localCategory || opts.category;
+ },
+ trackingOptions() {
+ const options = addExperimentContext(opts);
+ return { ...options, ...this.tracking };
+ },
+ },
+ methods: {
+ track(action, data = {}) {
+ const category = data.category || this.trackingCategory;
+ const options = {
+ ...this.trackingOptions,
+ ...data,
+ };
+
+ Tracking.event(category, action, options);
+ },
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/tracking/utils.js b/app/assets/javascripts/tracking/utils.js
new file mode 100644
index 00000000000..1189b2168ad
--- /dev/null
+++ b/app/assets/javascripts/tracking/utils.js
@@ -0,0 +1,102 @@
+import { omitBy, isUndefined } from 'lodash';
+import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
+import { getExperimentData } from '~/experimentation/utils';
+import {
+ ACTION_ATTR_SELECTOR,
+ LOAD_ACTION_ATTR_SELECTOR,
+ DEPRECATED_EVENT_ATTR_SELECTOR,
+ DEPRECATED_LOAD_EVENT_ATTR_SELECTOR,
+} from './constants';
+
+export const addExperimentContext = (opts) => {
+ const { experiment, ...options } = opts;
+
+ if (experiment) {
+ const data = getExperimentData(experiment);
+ if (data) {
+ const context = { schema: TRACKING_CONTEXT_SCHEMA, data };
+ return { ...options, context };
+ }
+ }
+
+ return options;
+};
+
+export const createEventPayload = (el, { suffix = '' } = {}) => {
+ const {
+ trackAction,
+ trackEvent,
+ trackValue,
+ trackExtra,
+ trackExperiment,
+ trackContext,
+ trackLabel,
+ trackProperty,
+ } = el?.dataset || {};
+
+ const action = (trackAction || trackEvent) + (suffix || '');
+ let value = trackValue || el.value || undefined;
+
+ if (el.type === 'checkbox' && !el.checked) {
+ value = 0;
+ }
+
+ let extra = trackExtra;
+
+ if (extra !== undefined) {
+ try {
+ extra = JSON.parse(extra);
+ } catch (e) {
+ extra = undefined;
+ }
+ }
+
+ const context = addExperimentContext({
+ experiment: trackExperiment,
+ context: trackContext,
+ });
+
+ const data = {
+ label: trackLabel,
+ property: trackProperty,
+ value,
+ extra,
+ ...context,
+ };
+
+ return {
+ action,
+ data: omitBy(data, isUndefined),
+ };
+};
+
+export const eventHandler = (e, func, opts = {}) => {
+ const actionSelector = `${ACTION_ATTR_SELECTOR}:not(${LOAD_ACTION_ATTR_SELECTOR})`;
+ const deprecatedEventSelector = `${DEPRECATED_EVENT_ATTR_SELECTOR}:not(${DEPRECATED_LOAD_EVENT_ATTR_SELECTOR})`;
+ const el = e.target.closest(`${actionSelector}, ${deprecatedEventSelector}`);
+
+ if (!el) {
+ return;
+ }
+
+ const { action, data } = createEventPayload(el, opts);
+ func(opts.category, action, data);
+};
+
+export const getEventHandlers = (category, func) => {
+ const handler = (opts) => (e) => eventHandler(e, func, { ...{ category }, ...opts });
+ const handlers = [];
+
+ handlers.push({ name: 'click', func: handler() });
+ handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
+ handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
+
+ return handlers;
+};
+
+export const renameKey = (o, oldKey, newKey) => {
+ const ret = {};
+ delete Object.assign(ret, o, { [newKey]: o[oldKey] })[oldKey];
+
+ return ret;
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
index d79da9d3b90..41edbc83cdb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue
@@ -20,21 +20,6 @@ export default {
type: Boolean,
required: true,
},
- showVisualReviewApp: {
- type: Boolean,
- required: false,
- default: false,
- },
- visualReviewAppMeta: {
- type: Object,
- required: false,
- default: () => ({
- sourceProjectId: '',
- sourceProjectPath: '',
- mergeRequestId: '',
- appUrl: '',
- }),
- },
},
computed: {
computedDeploymentStatus() {
@@ -63,8 +48,6 @@ export default {
<deployment-actions
:deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
- :show-visual-review-app="showVisualReviewApp"
- :visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index 7e587663c26..5ef7c2f72e0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -33,21 +33,6 @@ export default {
type: Object,
required: true,
},
- showVisualReviewApp: {
- type: Boolean,
- required: false,
- default: false,
- },
- visualReviewAppMeta: {
- type: Object,
- required: false,
- default: () => ({
- sourceProjectId: '',
- sourceProjectPath: '',
- mergeRequestId: '',
- appUrl: '',
- }),
- },
},
data() {
return {
@@ -178,8 +163,6 @@ export default {
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
- :show-visual-review-app="showVisualReviewApp"
- :visual-review-app-meta="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
index d23c7f016fb..d3384903cce 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
@@ -32,11 +32,6 @@ export default {
appUrl: '',
}),
},
- showVisualReviewAppLink: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
showCollapsedDeployments() {
@@ -74,8 +69,6 @@ export default {
class="gl-bg-gray-50"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
- :show-visual-review-app="showVisualReviewAppLink"
- :visual-review-app-meta="visualReviewAppMeta"
/>
</mr-collapsible-extension>
<div v-else class="mr-widget-extension">
@@ -85,8 +78,6 @@ export default {
:class="deploymentClass"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
- :show-visual-review-app="showVisualReviewAppLink"
- :visual-review-app-meta="visualReviewAppMeta"
/>
</div>
</template>
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 459bee8023f..1e363b0f5fb 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
@@ -20,8 +20,6 @@ export default {
GlLink,
GlSearchBoxByType,
ReviewAppLink,
- VisualReviewAppLink: () =>
- import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
autofocusonshow,
@@ -35,21 +33,6 @@ export default {
type: Object,
required: true,
},
- showVisualReviewApp: {
- type: Boolean,
- required: false,
- default: false,
- },
- visualReviewAppMeta: {
- type: Object,
- required: false,
- default: () => ({
- sourceProjectId: '',
- sourceProjectPath: '',
- mergeRequestId: '',
- appUrl: '',
- }),
- },
},
data() {
return { searchTerm: '' };
@@ -114,12 +97,5 @@ export default {
size="small"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3"
/>
- <visual-review-app-link
- v-if="showVisualReviewApp"
- :view-app-display="appButtonText"
- :link="deploymentExternalUrl"
- :app-metadata="visualReviewAppMeta"
- :changes="deployment.changes"
- />
</span>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index 5ed699acddf..f71b1fbc539 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -28,7 +28,12 @@ export default {
};
</script>
<template>
- <a v-gl-tooltip :href="authorUrl" :title="author.name" class="author-link inline">
+ <a
+ v-gl-tooltip
+ :href="authorUrl"
+ :title="showAuthorName ? null : author.name"
+ class="author-link inline"
+ >
<img :src="avatarUrl" class="avatar avatar-inline s16" />
<span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 5e401fc17e9..966262944ad 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -14,6 +14,7 @@ import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
@@ -30,6 +31,7 @@ export default {
GlDropdownItem,
GlLink,
GlSprintf,
+ WebIdeLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -56,31 +58,24 @@ export default {
});
},
webIdePath() {
- if (this.mr.canPushToSourceBranch) {
- return mergeUrlParams(
- {
- target_project:
- this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath
- ? this.mr.targetProjectFullPath
- : '',
- },
- webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`),
- );
- }
-
- return null;
- },
- ideButtonTitle() {
- return !this.mr.canPushToSourceBranch
- ? s__(
- 'mrWidget|You are not allowed to edit this project directly. Please fork to make changes.',
- )
- : '';
+ return mergeUrlParams(
+ {
+ target_project:
+ this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath
+ ? this.mr.targetProjectFullPath
+ : '',
+ },
+ webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`),
+ );
},
isFork() {
return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
},
},
+ i18n: {
+ webIdeText: s__('mrWidget|Open in Web IDE'),
+ gitpodText: s__('mrWidget|Open in Gitpod'),
+ },
};
</script>
<template>
@@ -123,22 +118,21 @@ export default {
<div class="branch-actions d-flex">
<template v-if="mr.isOpen">
- <span
+ <web-ide-link
v-if="!mr.sourceBranchRemoved"
- v-gl-tooltip
- :title="ideButtonTitle"
- class="gl-display-none d-md-inline-block gl-mr-3"
- :tabindex="ideButtonTitle ? 0 : null"
- >
- <gl-button
- :href="webIdePath"
- :disabled="!mr.canPushToSourceBranch"
- class="js-web-ide"
- data-qa-selector="open_in_web_ide_button"
- >
- {{ s__('mrWidget|Open in Web IDE') }}
- </gl-button>
- </span>
+ :show-edit-button="false"
+ :show-web-ide-button="true"
+ :web-ide-url="webIdePath"
+ :web-ide-text="$options.i18n.webIdeText"
+ :show-gitpod-button="mr.showGitpodButton"
+ :gitpod-url="mr.gitpodUrl"
+ :gitpod-enabled="mr.gitpodEnabled"
+ :gitpod-text="$options.i18n.gitpodText"
+ class="gl-display-none gl-md-display-inline-block gl-mr-3"
+ data-placement="bottom"
+ tabindex="0"
+ data-qa-selector="open_in_web_ide_button"
+ />
<gl-button
v-gl-modal-directive="'modal-merge-info'"
:disabled="mr.sourceBranchRemoved"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index c24ae92db4f..a8272002f16 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -66,11 +66,6 @@ export default {
pipeline() {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
- showVisualReviewAppLink() {
- return Boolean(
- this.mr.visualReviewAppAvailable && this.glFeatures.anonymousVisualReviewFeedback,
- );
- },
showMergeTrainPositionIndicator() {
return isNumber(this.mr.mergeTrainIndex);
},
@@ -120,8 +115,6 @@ export default {
:deployments="deployments"
:deployment-class="deploymentClass"
:has-deployment-metrics="hasDeploymentMetrics"
- :visual-review-app-meta="visualReviewAppMeta"
- :show-visual-review-app-link="showVisualReviewAppLink"
/>
<merge-train-position-indicator
v-if="showMergeTrainPositionIndicator"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 43317130b08..ac6368a3025 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,6 +1,6 @@
<script>
/* eslint-disable vue/no-v-html */
-import { s__ } from '~/locale';
+import { s__, n__ } from '~/locale';
export default {
name: 'MRWidgetRelatedLinks',
@@ -24,7 +24,8 @@ export default {
if (this.state === 'closed') {
return s__('mrWidget|Did not close');
}
- return s__('mrWidget|Closes');
+
+ return n__('mrWidget|Closes issue', 'mrWidget|Closes issues', this.relatedLinks.closingCount);
},
},
};
@@ -33,7 +34,8 @@ export default {
<section class="mr-info-list gl-ml-7 gl-pb-5">
<p v-if="relatedLinks.closing">{{ closesText }} <span v-html="relatedLinks.closing"></span></p>
<p v-if="relatedLinks.mentioned">
- {{ s__('mrWidget|Mentions') }} <span v-html="relatedLinks.mentioned"></span>
+ {{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
+ <span v-html="relatedLinks.mentioned"></span>
</p>
<p v-if="relatedLinks.assignToMe"><span v-html="relatedLinks.assignToMe"></span></p>
</section>
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 f99b825ff30..0eb173edbcb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlIcon, GlButton, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import createFlash from '~/flash';
@@ -10,7 +10,6 @@ import { AUTO_MERGE_STRATEGIES } from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MrWidgetAuthor from '../mr_widget_author.vue';
-import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeEnabled',
@@ -28,21 +27,20 @@ export default {
},
components: {
MrWidgetAuthor,
- statusIcon,
- GlLoadingIcon,
GlSkeletonLoader,
+ GlIcon,
+ GlButton,
+ GlSprintf,
},
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
service: {
type: Object,
required: true,
- default: () => ({}),
},
},
data() {
@@ -155,54 +153,44 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <status-icon status="success" />
+ <gl-icon name="status_scheduled" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" />
<div class="media-body">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
- <span class="js-status-text-before-author" data-testid="beforeStatusText">{{
- statusTextBeforeAuthor
- }}</span>
- <mr-widget-author :author="mergeUser" />
- <span class="js-status-text-after-author" data-testid="afterStatusText">{{
- statusTextAfterAuthor
- }}</span>
+ <gl-sprintf :message="statusText" data-testid="statusText">
+ <template #merge_author>
+ <mr-widget-author :author="mergeUser" />
+ </template>
+ </gl-sprintf>
</span>
- <a
+ <gl-button
v-if="mr.canCancelAutomaticMerge"
- :disabled="isCancellingAutoMerge"
- role="button"
- href="#"
- class="btn btn-sm btn-default js-cancel-auto-merge"
+ :loading="isCancellingAutoMerge"
+ size="small"
+ class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
- @click.prevent="cancelAutomaticMerge"
+ @click="cancelAutomaticMerge"
>
- <gl-loading-icon v-if="isCancellingAutoMerge" size="sm" inline class="gl-mr-1" />
{{ cancelButtonText }}
- </a>
+ </gl-button>
</h4>
<section class="mr-info-list">
- <p>
- {{ s__('mrWidget|The changes will be merged into') }}
- <a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
- </p>
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
- <a
+ <gl-button
v-if="canRemoveSourceBranch"
- :disabled="isRemovingSourceBranch"
- role="button"
- class="btn btn-sm btn-default js-remove-source-branch"
- href="#"
+ :loading="isRemovingSourceBranch"
+ size="small"
+ class="js-remove-source-branch"
data-testid="removeSourceBranchButton"
- @click.prevent="removeSourceBranch"
+ @click="removeSourceBranch"
>
- <gl-loading-icon v-if="isRemovingSourceBranch" size="sm" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
- </a>
+ </gl-button>
</p>
</section>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 302a30dab54..6d5ca58aa20 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -14,7 +14,6 @@ export default {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 5a93021978c..1596f852b74 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -45,7 +45,6 @@ export default {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
},
data() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index e973a2350a3..42e9261b82c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -17,7 +17,6 @@ export default {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 5177eab790b..a1759b1a815 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -25,12 +25,10 @@ export default {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
service: {
type: Object,
required: true,
- default: () => ({}),
},
},
data() {
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 32749b8b018..1c245b584ea 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
@@ -11,7 +11,6 @@ export default {
mr: {
type: Object,
required: true,
- default: () => ({}),
},
},
data() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 2d0b7fe46a6..f33f4d3fda0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -188,13 +188,6 @@ export default {
return this.mr.preferredAutoMergeStrategy;
},
- isSHAMismatch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.mr.sha !== this.state.diffHeadSha;
- }
-
- return this.mr.isSHAMismatch;
- },
squashIsSelected() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.isSquashReadOnly ? this.state.squashOnMerge : this.state.squash;
@@ -573,21 +566,6 @@ export default {
</div>
</template>
</div>
- <div v-if="isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch">
- <gl-icon name="warning-solid" class="text-warning mr-1" />
- <span class="text-warning">
- <gl-sprintf
- :message="
- __('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}')
- "
- >
- <template #link="{ content }">
- <gl-link :href="mr.mergeRequestDiffsPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- </div>
-
<div
v-if="showDangerMessageForMergeTrain"
class="gl-mt-5 gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 89edf588213..7eeba8d8f89 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -1,24 +1,42 @@
<script>
+import { GlButton } from '@gitlab/ui';
+import { I18N_SHA_MISMATCH } from '../../i18n';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'ShaMismatch',
components: {
statusIcon,
+ GlButton,
+ },
+ i18n: {
+ I18N_SHA_MISMATCH,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
},
};
</script>
<template>
<div class="mr-widget-body media">
- <status-icon :show-disabled-button="true" status="warning" />
- <div class="media-body space-children">
- <span class="bold" data-qa-selector="head_mismatch_content">
- {{
- s__(`mrWidget|The source branch HEAD has recently changed.
-Please reload the page and review the changes before merging`)
- }}
+ <status-icon :show-disabled-button="false" status="warning" />
+ <div class="media-body">
+ <span class="gl-font-weight-bold" data-qa-selector="head_mismatch_content">
+ {{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }}
</span>
+ <gl-button
+ class="gl-ml-3"
+ data-testid="action-button"
+ size="small"
+ category="primary"
+ variant="confirm"
+ :href="mr.mergeRequestDiffsPath"
+ >{{ $options.i18n.I18N_SHA_MISMATCH.actionButtonLabel }}</gl-button
+ >
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js
index e8e522a01e9..c88e795e5f3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/i18n.js
+++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js
@@ -5,3 +5,8 @@ export const SQUASH_BEFORE_MERGE = {
checkboxLabel: __('Squash commits'),
helpLabel: __('What is squashing?'),
};
+
+export const I18N_SHA_MISMATCH = {
+ warningMessage: __('Merge blocked: new changes were just added.'),
+ actionButtonLabel: __('Review changes'),
+};
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 67d9892d9c6..de77ed7ec9c 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
@@ -2,14 +2,13 @@ import { s__ } from '~/locale';
export default {
computed: {
- statusTextBeforeAuthor() {
- return s__('mrWidget|Set by');
- },
- statusTextAfterAuthor() {
- return s__('mrWidget|to be merged automatically when the pipeline succeeds');
+ statusText() {
+ return s__(
+ 'mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds',
+ );
},
cancelButtonText() {
- return s__('mrWidget|Cancel');
+ return s__('mrWidget|Cancel auto-merge');
},
},
};
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 5fe04269e33..a8a9df598f5 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
@@ -38,6 +38,7 @@ import RebaseState from './components/states/mr_widget_rebase.vue';
import NothingToMergeState from './components/states/nothing_to_merge.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue';
import ReadyToMergeState from './components/states/ready_to_merge.vue';
+import ShaMismatch from './components/states/sha_mismatch.vue';
import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import WorkInProgressState from './components/states/work_in_progress.vue';
// import ExtensionsContainer from './components/extensions/container';
@@ -72,7 +73,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
- 'sha-mismatch': ReadyToMergeState,
+ 'sha-mismatch': ShaMismatch,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState,
@@ -150,7 +151,7 @@ export default {
);
},
shouldRenderCodeQuality() {
- return this.mr?.codeclimate?.head_path;
+ return this.mr?.codequalityReportsPath;
},
shouldRenderRelatedLinks() {
return Boolean(this.mr.relatedLinks) && !this.mr.isNothingToMergeState;
@@ -496,8 +497,6 @@ export default {
<!-- <extensions-container :mr="mr" /> -->
<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"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 9d3f4eb01ed..04800cf43f0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -23,8 +23,8 @@ export default function deviseState() {
return stateKey.pipelineBlocked;
} else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
- } else if (this.autoMergeEnabled) {
- return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled;
+ } else if (this.autoMergeEnabled && !this.mergeError) {
+ return stateKey.autoMergeEnabled;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
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 8e3160ce2f2..8979fe621ac 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
@@ -19,6 +19,7 @@ export default class MergeRequestStore {
this.setPaths(data);
this.setData(data);
+ this.setGitpodData(data);
}
setData(data, isRebased) {
@@ -71,7 +72,13 @@ export default class MergeRequestStore {
const assignToMe = links.assign_to_closing;
if (closing || mentioned || assignToMe) {
- this.relatedLinks = { closing, mentioned, assignToMe };
+ this.relatedLinks = {
+ closing,
+ mentioned,
+ assignToMe,
+ closingCount: links.closing_count,
+ mentionedCount: links.mentioned_count,
+ };
}
}
@@ -199,6 +206,12 @@ export default class MergeRequestStore {
}
}
+ setGitpodData(data) {
+ this.showGitpodButton = data.show_gitpod_button;
+ this.gitpodUrl = data.gitpod_url;
+ this.gitpodEnabled = data.gitpod_enabled;
+ }
+
setState() {
if (this.mergeOngoing) {
this.state = 'merging';
@@ -261,7 +274,6 @@ export default class MergeRequestStore {
this.baseBlobPath = blobPath.base_path || '';
this.codequalityReportsPath = data.codequality_reports_path;
this.codequalityHelpPath = data.codequality_help_path;
- this.codeclimate = data.codeclimate;
// Security reports
this.sastComparisonPath = data.sast_comparison_path;
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
new file mode 100644
index 00000000000..eeed5e9dc3a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
@@ -0,0 +1,27 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import { __ } from '~/locale';
+import DropdownWidget from './dropdown_widget.vue';
+
+export default {
+ component: DropdownWidget,
+ title: 'vue_shared/components/dropdown/dropdown_widget/dropdown_widget',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { DropdownWidget },
+ props: Object.keys(argTypes),
+ template: '<dropdown-widget v-bind="$props" v-on="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ options: [
+ { id: 'gid://gitlab/Milestone/-1', title: __('Any Milestone') },
+ { id: 'gid://gitlab/Milestone/0', title: __('No Milestone') },
+ { id: 'gid://gitlab/Milestone/-2', title: __('Upcoming') },
+ { id: 'gid://gitlab/Milestone/-3', title: __('Started') },
+ ],
+ selectText: 'Select',
+ searchText: 'Search',
+};
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
new file mode 100644
index 00000000000..7859ef85dd8
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -0,0 +1,165 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownForm,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ selectText: {
+ type: String,
+ required: false,
+ default: __('Select'),
+ },
+ searchText: {
+ type: String,
+ required: false,
+ default: __('Search'),
+ },
+ presetOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selected: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ searchTerm: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isSearchEmpty() {
+ return this.searchTerm === '' && !this.isLoading;
+ },
+ noOptionsFound() {
+ return !this.isSearchEmpty && this.options.length === 0;
+ },
+ },
+ methods: {
+ selectOption(option) {
+ this.$emit('set-option', option || null);
+ },
+ isSelected(option) {
+ return (
+ this.selected &&
+ ((option.name && this.selected.name === option.name) ||
+ (option.title && this.selected.title === option.title))
+ );
+ },
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
+ setFocus() {
+ this.$refs.search.focusInput();
+ },
+ setSearchTerm(search) {
+ this.$emit('set-search', search);
+ },
+ avatarUrl(option) {
+ return option.avatar_url || option.avatarUrl || null;
+ },
+ secondaryText(option) {
+ // TODO: this has some knowledge of the context where the component is used. We could later rework it.
+ return option.username || null;
+ },
+ },
+ i18n: {
+ noMatchingResults: __('No matching results'),
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="dropdown"
+ :text="selectText"
+ lazy
+ menu-class="gl-w-full!"
+ class="gl-w-full"
+ v-on="$listeners"
+ @shown="setFocus"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ ref="search"
+ :value="searchTerm"
+ :placeholder="searchText"
+ class="js-dropdown-input-field"
+ @input="setSearchTerm"
+ />
+ </template>
+ <gl-dropdown-form class="gl-relative gl-min-h-7">
+ <gl-loading-icon
+ v-if="isLoading"
+ size="md"
+ class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
+ />
+ <template v-else>
+ <template v-if="isSearchEmpty && presetOptions.length > 0">
+ <gl-dropdown-item
+ v-for="option in presetOptions"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ @click="selectOption(option)"
+ >
+ <slot name="preset-item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
+ <gl-dropdown-item
+ v-for="option in options"
+ :key="option.id"
+ :is-checked="isSelected(option)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ :avatar-url="avatarUrl(option)"
+ :secondary-text="secondaryText(option)"
+ data-testid="unselected-option"
+ @click="selectOption(option)"
+ >
+ <slot name="item" :item="option">
+ {{ option.title }}
+ </slot>
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
+ {{ $options.i18n.noMatchingResults }}
+ </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/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 994ce6a762a..2e9634819a0 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
@@ -2,10 +2,14 @@ import { __ } from '~/locale';
export const DEBOUNCE_DELAY = 200;
export const MAX_RECENT_TOKENS_SIZE = 3;
+export const WEIGHT_TOKEN_SUGGESTIONS_SIZE = 21;
export const FILTER_NONE = 'None';
export const FILTER_ANY = 'Any';
export const FILTER_CURRENT = 'Current';
+export const FILTER_UPCOMING = 'Upcoming';
+export const FILTER_STARTED = 'Started';
+export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
@@ -24,11 +28,9 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
-export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
-
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
+ { value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
+ { value: FILTER_STARTED, text: __(FILTER_STARTED) },
]);
export const SortDirection = {
@@ -36,12 +38,14 @@ export const SortDirection = {
ascending: 'ascending',
};
+export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
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_TYPE = __('Type');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_ITERATION = __('Iteration');
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 5ab287150f2..9dc5c5db276 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
@@ -16,7 +16,7 @@ import createFlash from '~/flash';
import { __ } from '~/locale';
import { SortDirection } from './constants';
-import { stripQuotes, uniqueTokens } from './filtered_search_utils';
+import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
components: {
@@ -223,9 +223,14 @@ export default {
// Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones
- const resultantSearches = this.recentSearchesStore.setRecentSearches(
+ let resultantSearches = this.recentSearchesStore.setRecentSearches(
this.recentSearchesStore.state.recentSearches.concat(searches),
);
+ // If visited URL has search params, add them to recent search store
+ if (filterEmptySearchTerm(this.filterValue).length) {
+ resultantSearches = this.recentSearchesStore.addRecentSearch(this.filterValue);
+ }
+
this.recentSearchesService.save(resultantSearches);
this.recentSearches = resultantSearches;
});
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 571d24b50cf..6573f366b52 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
@@ -247,3 +247,12 @@ export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenVa
);
}
}
+
+/**
+ * Removes `FILTERED_SEARCH_TERM` tokens with empty data
+ *
+ * @param filterTokens array of filtered search tokens
+ * @return {Array} array of filtered search tokens
+ */
+export const filterEmptySearchTerm = (filterTokens = []) =>
+ filterTokens.filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index a25a19a006c..ae5d3965de1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -31,19 +31,25 @@ export default {
data() {
return {
authors: this.config.initialAuthors || [],
- defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
- preloadedAuthors: this.config.preloadedAuthors || [],
loading: false,
};
},
+ computed: {
+ defaultAuthors() {
+ return this.config.defaultAuthors || [DEFAULT_LABEL_ANY];
+ },
+ preloadedAuthors() {
+ return this.config.preloadedAuthors || [];
+ },
+ },
methods: {
- getActiveAuthor(authors, currentValue) {
- return authors.find((author) => author.username.toLowerCase() === currentValue);
+ getActiveAuthor(authors, data) {
+ return authors.find((author) => author.username.toLowerCase() === data.toLowerCase());
},
getAvatarUrl(author) {
return author.avatarUrl || author.avatar_url;
},
- fetchAuthorBySearchTerm(searchTerm) {
+ fetchAuthors(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
? this.config.fetchAuthors(this.config.fetchPath, searchTerm)
@@ -76,11 +82,11 @@ export default {
:active="active"
:suggestions-loading="loading"
:suggestions="authors"
- :fn-active-token-value="getActiveAuthor"
+ :get-active-token-value="getActiveAuthor"
:default-suggestions="defaultAuthors"
:preloaded-suggestions="preloadedAuthors"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- @fetch-suggestions="fetchAuthorBySearchTerm"
+ @fetch-suggestions="fetchAuthors"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@@ -91,7 +97,7 @@ export default {
shape="circle"
class="gl-mr-2"
/>
- <span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span>
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
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
index a4804525a53..d1326e96794 100644
--- 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
@@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { DEBOUNCE_DELAY } from '../constants';
+import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@@ -42,12 +42,10 @@ export default {
required: false,
default: () => [],
},
- fnActiveTokenValue: {
+ getActiveTokenValue: {
type: Function,
required: false,
- default: (suggestions, currentTokenValue) => {
- return suggestions.find(({ value }) => value === currentTokenValue);
- },
+ default: (suggestions, data) => suggestions.find(({ value }) => value === data),
},
defaultSuggestions: {
type: Array,
@@ -69,11 +67,6 @@ export default {
required: false,
default: 'id',
},
- fnCurrentTokenValue: {
- type: Function,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -81,7 +74,6 @@ export default {
recentSuggestions: this.recentSuggestionsStorageKey
? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
: [],
- loading: false,
};
},
computed: {
@@ -94,14 +86,16 @@ export default {
preloadedTokenIds() {
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
- currentTokenValue() {
- if (this.fnCurrentTokenValue) {
- return this.fnCurrentTokenValue(this.value.data);
- }
- return this.value.data.toLowerCase();
- },
activeTokenValue() {
- return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue);
+ return this.getActiveTokenValue(this.suggestions, this.value.data);
+ },
+ availableDefaultSuggestions() {
+ if (this.value.operator === OPERATOR_IS_NOT) {
+ return this.defaultSuggestions.filter(
+ (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ );
+ }
+ return this.defaultSuggestions;
},
/**
* Return all the suggestions when searchKey is present
@@ -117,6 +111,29 @@ export default {
!this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]),
);
},
+ showDefaultSuggestions() {
+ return this.availableDefaultSuggestions.length;
+ },
+ showRecentSuggestions() {
+ return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
+ },
+ showPreloadedSuggestions() {
+ return this.preloadedSuggestions.length && !this.searchKey;
+ },
+ showAvailableSuggestions() {
+ return this.availableSuggestions.length;
+ },
+ showSuggestions() {
+ // These conditions must match the template under `#suggestions` slot
+ // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65817#note_632619411
+ return (
+ this.showDefaultSuggestions ||
+ this.showRecentSuggestions ||
+ this.showPreloadedSuggestions ||
+ this.suggestionsLoading ||
+ this.showAvailableSuggestions
+ );
+ },
},
watch: {
active: {
@@ -168,10 +185,10 @@ export default {
<template #view="viewTokenProps">
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
</template>
- <template #suggestions>
- <template v-if="defaultSuggestions.length">
+ <template v-if="showSuggestions" #suggestions>
+ <template v-if="showDefaultSuggestions">
<gl-filtered-search-suggestion
- v-for="token in defaultSuggestions"
+ v-for="token in availableDefaultSuggestions"
:key="token.value"
:value="token.value"
>
@@ -179,13 +196,13 @@ export default {
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
</template>
- <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey">
+ <template v-if="showRecentSuggestions">
<gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
<slot name="suggestions-list" :suggestions="recentSuggestions"></slot>
<gl-dropdown-divider />
</template>
<slot
- v-if="preloadedSuggestions.length && !searchKey"
+ v-if="showPreloadedSuggestions"
name="suggestions-list"
:suggestions="preloadedSuggestions"
></slot>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index 5859fd10688..4ecfc1cf40c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -1,27 +1,19 @@
<script>
-import {
- GlToken,
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-
-import { DEBOUNCE_DELAY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
export default {
components: {
- GlToken,
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -34,82 +26,62 @@ export default {
data() {
return {
branches: this.config.initialBranches || [],
- defaultBranches: this.config.defaultBranches || [],
- loading: true,
+ loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeBranch() {
- return this.branches.find((branch) => branch.name.toLowerCase() === this.currentValue);
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.branches.length) {
- this.fetchBranchBySearchTerm(this.value.data);
- }
- },
+ defaultBranches() {
+ return this.config.defaultBranches || [];
},
},
methods: {
- fetchBranchBySearchTerm(searchTerm) {
+ getActiveBranch(branches, data) {
+ return branches.find((branch) => branch.name.toLowerCase() === data.toLowerCase());
+ },
+ fetchBranches(searchTerm) {
this.loading = true;
this.config
.fetchBranches(searchTerm)
.then(({ data }) => {
this.branches = data;
})
- .catch(() => createFlash({ message: __('There was a problem fetching branches.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching branches.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchBranches: debounce(function debouncedSearch({ data }) {
- this.fetchBranchBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultBranches"
+ :suggestions="branches"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveBranch"
+ @fetch-suggestions="fetchBranches"
v-on="$listeners"
- @input="searchBranches"
>
- <template #view-token="{ inputValue }">
- <gl-token variant="search-value">{{
- activeBranch ? activeBranch.name : inputValue
- }}</gl-token>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="branch in defaultBranches"
- :key="branch.value"
- :value="branch.value"
+ v-for="branch in suggestions"
+ :key="branch.id"
+ :value="branch.name"
>
- {{ branch.text }}
+ <div class="gl-display-flex">
+ <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
+ {{ branch.name }}
+ </div>
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultBranches.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="branch in branches"
- :key="branch.id"
- :value="branch.name"
- >
- <div class="gl-display-flex">
- <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span>
- <div>{{ branch.name }}</div>
- </div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-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 d186f46866c..5a69751a2cc 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -1,26 +1,21 @@
<script>
-import {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
-
-import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -33,87 +28,63 @@ export default {
data() {
return {
emojis: this.config.initialEmojis || [],
- defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY,
- loading: true,
+ loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeEmoji() {
- return this.emojis.find(
- (emoji) => emoji.name.toLowerCase() === stripQuotes(this.currentValue),
- );
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.emojis.length) {
- this.fetchEmojiBySearchTerm(this.value.data);
- }
- },
+ defaultEmojis() {
+ return this.config.defaultEmojis || DEFAULT_NONE_ANY;
},
},
methods: {
- fetchEmojiBySearchTerm(searchTerm) {
+ getActiveEmoji(emojis, data) {
+ return emojis.find((emoji) => emoji.name.toLowerCase() === stripQuotes(data).toLowerCase());
+ },
+ fetchEmojis(searchTerm) {
this.loading = true;
this.config
.fetchEmojis(searchTerm)
- .then((res) => {
- this.emojis = Array.isArray(res) ? res : res.data;
+ .then((response) => {
+ this.emojis = Array.isArray(response) ? response : response.data;
+ })
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching emojis.') });
})
- .catch(() =>
- createFlash({
- message: __('There was a problem fetching emojis.'),
- }),
- )
.finally(() => {
this.loading = false;
});
},
- searchEmojis: debounce(function debouncedSearch({ data }) {
- this.fetchEmojiBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultEmojis"
+ :suggestions="emojis"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveEmoji"
+ @fetch-suggestions="fetchEmojis"
v-on="$listeners"
- @input="searchEmojis"
>
- <template #view="{ inputValue }">
- <gl-emoji v-if="activeEmoji" :data-name="activeEmoji.name" />
- <span v-else>{{ inputValue }}</span>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ <gl-emoji v-if="activeTokenValue" :data-name="activeTokenValue.name" />
+ <template v-else>{{ inputValue }}</template>
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="emoji in defaultEmojis"
- :key="emoji.value"
- :value="emoji.value"
+ v-for="emoji in suggestions"
+ :key="emoji.name"
+ :value="emoji.name"
>
- {{ emoji.value }}
+ <div class="gl-display-flex">
+ <gl-emoji class="gl-mr-3" :data-name="emoji.name" />
+ {{ emoji.name }}
+ </div>
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultEmojis.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="emoji in emojis"
- :key="emoji.name"
- :value="emoji.name"
- >
- <div class="gl-display-flex">
- <gl-emoji :data-name="emoji.name" />
- <span class="gl-ml-3">{{ emoji.name }}</span>
- </div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
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 aa234cf86d9..9f68308808e 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
@@ -8,7 +8,7 @@ import {
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
+import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
export default {
separator: '::&',
@@ -48,6 +48,14 @@ export default {
defaultEpics() {
return this.config.defaultEpics || DEFAULT_NONE_ANY;
},
+ availableDefaultEpics() {
+ if (this.value.operator === OPERATOR_IS_NOT) {
+ return this.defaultEpics.filter(
+ (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ );
+ }
+ return this.defaultEpics;
+ },
activeEpic() {
if (this.currentValue && this.epics.length) {
// Check if current value is an epic ID.
@@ -99,7 +107,7 @@ export default {
// We don't have any information about selected token except for its
// group path and iid joined by separator, so we need to manually
// compose epic path from it.
- if (data.includes(this.$options.separator)) {
+ if (data.includes?.(this.$options.separator)) {
const [groupPath, epicIid] = data.split(this.$options.separator);
epicPath = `/groups/${groupPath}/-/epics/${epicIid}`;
}
@@ -127,13 +135,13 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
- v-for="epic in defaultEpics"
+ v-for="epic in availableDefaultEpics"
:key="epic.value"
:value="epic.value"
>
{{ epic.text }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultEpics.length" />
+ <gl-dropdown-divider v-if="availableDefaultEpics.length" />
<gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
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
index ba8b2421726..c1d1bc7da91 100644
--- 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
@@ -1,24 +1,21 @@
<script>
-import {
- GlDropdownDivider,
- GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_ITERATIONS } from '../constants';
export default {
components: {
- GlDropdownDivider,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -35,84 +32,58 @@ export default {
};
},
computed: {
- currentValue() {
- return this.value.data;
- },
- activeIteration() {
- return this.iterations.find(
- (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue),
- );
- },
defaultIterations() {
return this.config.defaultIterations || DEFAULT_ITERATIONS;
},
},
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.iterations.length) {
- this.fetchIterationBySearchTerm(this.currentValue);
- }
- },
- },
- },
methods: {
- getValue(iteration) {
- return String(getIdFromGraphQLId(iteration.id));
+ getActiveIteration(iterations, data) {
+ return iterations.find((iteration) => this.getValue(iteration) === data);
},
- fetchIterationBySearchTerm(searchTerm) {
- const fetchPromise = this.config.fetchPath
- ? this.config.fetchIterations(this.config.fetchPath, searchTerm)
- : this.config.fetchIterations(searchTerm);
-
+ fetchIterations(searchTerm) {
this.loading = true;
-
- fetchPromise
+ this.config
+ .fetchIterations(searchTerm)
.then((response) => {
this.iterations = Array.isArray(response) ? response : response.data;
})
- .catch(() => createFlash({ message: __('There was a problem fetching iterations.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching iterations.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchIterations: debounce(function debouncedSearch({ data }) {
- this.fetchIterationBySearchTerm(data);
- }, DEBOUNCE_DELAY),
+ getValue(iteration) {
+ return String(getIdFromGraphQLId(iteration.id));
+ },
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultIterations"
+ :suggestions="iterations"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveIteration"
+ @fetch-suggestions="fetchIterations"
v-on="$listeners"
- @input="searchIterations"
>
- <template #view="{ inputValue }">
- {{ activeIteration ? activeIteration.title : inputValue }}
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ {{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="iteration in defaultIterations"
- :key="iteration.value"
- :value="iteration.value"
+ v-for="iteration in suggestions"
+ :key="iteration.id"
+ :value="getValue(iteration)"
>
- {{ iteration.text }}
+ {{ iteration.title }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultIterations.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="iteration in iterations"
- :key="iteration.id"
- :value="getValue(iteration)"
- >
- {{ iteration.title }}
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 4d08f81fee9..c31f3a25fb1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -5,7 +5,7 @@ import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { DEFAULT_LABELS } from '../constants';
+import { DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
@@ -33,14 +33,18 @@ export default {
data() {
return {
labels: this.config.initialLabels || [],
- defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: false,
};
},
+ computed: {
+ defaultLabels() {
+ return this.config.defaultLabels || DEFAULT_NONE_ANY;
+ },
+ },
methods: {
- getActiveLabel(labels, currentValue) {
+ getActiveLabel(labels, data) {
return labels.find(
- (label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue),
+ (label) => this.getLabelName(label).toLowerCase() === stripQuotes(data).toLowerCase(),
);
},
/**
@@ -68,7 +72,7 @@ export default {
}
return {};
},
- fetchLabelBySearchTerm(searchTerm) {
+ fetchLabels(searchTerm) {
this.loading = true;
this.config
.fetchLabels(searchTerm)
@@ -98,10 +102,10 @@ export default {
:active="active"
:suggestions-loading="loading"
:suggestions="labels"
- :fn-active-token-value="getActiveLabel"
+ :get-active-token-value="getActiveLabel"
:default-suggestions="defaultLabels"
:recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
- @fetch-suggestions="fetchLabelBySearchTerm"
+ @fetch-suggestions="fetchLabels"
v-on="$listeners"
>
<template
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 66ad5ef5b4e..4b9ad6d8f91 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -1,27 +1,22 @@
<script>
-import {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
-
-import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_MILESTONES } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
- GlFilteredSearchToken,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -34,36 +29,21 @@ export default {
data() {
return {
milestones: this.config.initialMilestones || [],
- defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: false,
};
},
computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeMilestone() {
- return this.milestones.find(
- (milestone) => milestone.title.toLowerCase() === stripQuotes(this.currentValue),
- );
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.milestones.length) {
- this.fetchMilestoneBySearchTerm(this.value.data);
- }
- },
+ defaultMilestones() {
+ return this.config.defaultMilestones || DEFAULT_MILESTONES;
},
},
methods: {
- fetchMilestoneBySearchTerm(searchTerm = '') {
- if (this.loading) {
- return;
- }
-
+ getActiveMilestone(milestones, data) {
+ return milestones.find(
+ (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(),
+ );
+ },
+ fetchMilestones(searchTerm) {
this.loading = true;
this.config
.fetchMilestones(searchTerm)
@@ -71,47 +51,40 @@ export default {
const data = Array.isArray(response) ? response : response.data;
this.milestones = data.slice().sort(sortMilestonesByDueDate);
})
- .catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
+ .catch(() => {
+ createFlash({ message: __('There was a problem fetching milestones.') });
+ })
.finally(() => {
this.loading = false;
});
},
- searchMilestones: debounce(function debouncedSearch({ data }) {
- this.fetchMilestoneBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultMilestones"
+ :suggestions="milestones"
+ :suggestions-loading="loading"
+ :get-active-token-value="getActiveMilestone"
+ @fetch-suggestions="fetchMilestones"
v-on="$listeners"
- @input="searchMilestones"
>
- <template #view="{ inputValue }">
- <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span>
+ <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
+ %{{ activeTokenValue ? activeTokenValue.title : inputValue }}
</template>
- <template #suggestions>
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="milestone in defaultMilestones"
- :key="milestone.value"
- :value="milestone.value"
+ v-for="milestone in suggestions"
+ :key="milestone.id"
+ :value="milestone.title"
>
- {{ milestone.text }}
+ {{ milestone.title }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultMilestones.length" />
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="milestone in milestones"
- :key="milestone.id"
- :value="milestone.title"
- >
- <div>{{ milestone.title }}</div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-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
index 72116f0e991..280fb234576 100644
--- 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
@@ -1,15 +1,20 @@
<script>
-import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { GlFilteredSearchSuggestion } from '@gitlab/ui';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants';
+
+const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString());
export default {
- baseWeights: ['0', '1', '2', '3', '4', '5'],
components: {
- GlDropdownDivider,
+ BaseToken,
GlFilteredSearchSuggestion,
- GlFilteredSearchToken,
},
props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
config: {
type: Object,
required: true,
@@ -21,38 +26,41 @@ export default {
},
data() {
return {
- weights: this.$options.baseWeights,
- defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY,
+ weights,
};
},
+ computed: {
+ defaultWeights() {
+ return this.config.defaultWeights || DEFAULT_NONE_ANY;
+ },
+ },
methods: {
- updateWeights({ data }) {
- const weight = parseInt(data, 10);
- this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)];
+ getActiveWeight(weightSuggestions, data) {
+ return weightSuggestions.find((weight) => weight === data);
+ },
+ updateWeights(searchTerm) {
+ const weight = parseInt(searchTerm, 10);
+ this.weights = Number.isNaN(weight) ? weights : [String(weight)];
},
},
};
</script>
<template>
- <gl-filtered-search-token
+ <base-token
+ :active="active"
:config="config"
- v-bind="{ ...$props, ...$attrs }"
+ :value="value"
+ :default-suggestions="defaultWeights"
+ :suggestions="weights"
+ :get-active-token-value="getActiveWeight"
+ @fetch-suggestions="updateWeights"
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">
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight">
{{ weight }}
</gl-filtered-search-suggestion>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d343ba700ab..3ed9de6c133 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,5 +1,5 @@
<script>
-import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import $ from 'jquery';
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
@@ -10,7 +10,6 @@ import ToolbarButton from './toolbar_button.vue';
export default {
components: {
ToolbarButton,
- GlIcon,
GlPopover,
GlButton,
},
@@ -46,6 +45,7 @@ export default {
data() {
return {
tag: '> ',
+ suggestPopoverVisible: false,
};
},
computed: {
@@ -76,15 +76,27 @@ export default {
return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
},
},
+ watch: {
+ showSuggestPopover() {
+ this.updateSuggestPopoverVisibility();
+ },
+ },
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
+
+ this.updateSuggestPopoverVisibility();
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
+ async updateSuggestPopoverVisibility() {
+ await this.$nextTick();
+
+ this.suggestPopoverVisible = this.showSuggestPopover && this.canSuggest;
+ },
isValid(form) {
return (
!form ||
@@ -153,127 +165,114 @@ export default {
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
- <div class="d-inline-block">
- <toolbar-button
- tag="**"
- :button-title="
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.bold"
- icon="bold"
- />
- <toolbar-button
- tag="_"
- :button-title="
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.italic"
- icon="italic"
- />
+ <toolbar-button
+ tag="**"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.bold"
+ icon="bold"
+ />
+ <toolbar-button
+ tag="_"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.italic"
+ icon="italic"
+ />
+ <toolbar-button
+ :prepend="true"
+ :tag="tag"
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ @click="handleQuote"
+ />
+ <template v-if="canSuggest">
<toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
:prepend="true"
- :tag="tag"
- :button-title="__('Insert a quote')"
- icon="quote"
- @click="handleQuote"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ icon="doc-code"
+ data-qa-selector="suggestion_button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
/>
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <template v-if="canSuggest">
- <toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
- :prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- icon="doc-code"
- data-qa-selector="suggestion_button"
- class="js-suggestion-btn"
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="info"
+ category="primary"
+ size="small"
@click="handleSuggestDismissed"
- />
- <gl-popover
- v-if="showSuggestPopover && $refs.suggestButton"
- :target="$refs.suggestButton"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="showSuggestPopover"
>
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="info"
- category="primary"
- size="sm"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
- <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
- <toolbar-button
- tag="[{text}](url)"
- tag-select="url"
- :button-title="
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
- "
- :shortcuts="$options.shortcuts.link"
- icon="link"
- />
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <toolbar-button
- :prepend="true"
- tag="- "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- />
- <toolbar-button
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- />
- <toolbar-button
- :prepend="true"
- tag="- [ ] "
- :button-title="__('Add a task list')"
- 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')"
- icon="table"
- />
- </div>
- <div class="d-inline-block ml-md-2 ml-0">
- <button
- v-gl-tooltip
- :aria-label="__('Go full screen')"
- class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
- data-container="body"
- tabindex="-1"
- :title="__('Go full screen')"
- type="button"
- >
- <gl-icon name="maximize" />
- </button>
- </div>
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </template>
+ <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
+ <toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
+ "
+ :shortcuts="$options.shortcuts.link"
+ icon="link"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="- "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ />
+ <toolbar-button
+ :prepend="true"
+ tag="- [ ] "
+ :button-title="__('Add a task list')"
+ 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')"
+ icon="table"
+ />
+ <toolbar-button
+ class="js-zen-enter"
+ :prepend="true"
+ :button-title="__('Go full screen')"
+ icon="maximize"
+ />
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 6c35741e7e5..6a83939795c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -1,9 +1,9 @@
<script>
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
components: {
- GlIcon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -19,7 +19,8 @@ export default {
},
tag: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
tagBlock: {
type: String,
@@ -71,7 +72,7 @@ export default {
</script>
<template>
- <button
+ <gl-button
v-gl-tooltip
:data-md-tag="tag"
:data-md-cursor-offset="cursorOffset"
@@ -82,11 +83,11 @@ export default {
:data-md-shortcuts="shortcutsString"
:title="buttonTitle"
:aria-label="buttonTitle"
+ :icon="icon"
type="button"
- class="toolbar-btn js-md"
+ category="tertiary"
+ class="js-md"
data-container="body"
@click="() => $emit('click')"
- >
- <gl-icon :name="icon" />
- </button>
+ />
</template>
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 79a9e1fca8c..8a67754993d 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
@@ -42,12 +42,12 @@ export default {
itemsCount: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
pageInfo: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
statusTabs: {
type: Array,
diff --git a/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue b/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue
new file mode 100644
index 00000000000..fa11661255f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/papa_parse_alert.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlAlert,
+ },
+ i18n: {
+ genericErrorMessage: s__('CsvParser|Failed to render the CSV file for the following reasons:'),
+ MissingQuotes: s__('CsvParser|Quoted field unterminated'),
+ InvalidQuotes: s__('CsvParser|Trailing quote on quoted field is malformed'),
+ UndetectableDelimiter: s__('CsvParser|Unable to auto-detect delimiter; defaulted to ","'),
+ TooManyFields: s__('CsvParser|Too many fields'),
+ TooFewFields: s__('CsvParser|Too few fields'),
+ },
+ props: {
+ papaParseErrors: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ errorMessages() {
+ const errorMessages = this.papaParseErrors.map(
+ (error) => this.$options.i18n[error.code] ?? error.message,
+ );
+ return new Set(errorMessages);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="danger" :dismissible="false">
+ {{ $options.i18n.genericErrorMessage }}
+ <ul class="gl-mb-0!">
+ <li v-for="error in errorMessages" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index a0c5a0559de..f21092af501 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -15,6 +15,11 @@ export default {
ProjectListItem,
},
props: {
+ maxListHeight: {
+ type: Number,
+ required: false,
+ default: 402,
+ },
projectSearchResults: {
type: Array,
required: true,
@@ -101,7 +106,7 @@ export default {
<div class="d-flex flex-column">
<gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
- :max-list-height="402"
+ :max-list-height="maxListHeight"
:fetched-items="projectSearchResults.length"
:total-items="totalResults"
@bottomReached="bottomReached"
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 9914bfc6026..623e7799493 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
@@ -132,6 +132,9 @@ export default {
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
this.searchKey = '';
+
+ // Prevent parent form submission upon hitting enter.
+ e.preventDefault();
} else if (e.keyCode === ESC_KEY_CODE) {
this.toggleDropdownContents();
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index aad754e15b0..7989ad40b5a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
- variant="link"
- class="float-right gl-text-gray-900! gl-hover-text-blue-800! js-sidebar-dropdown-toggle"
+ category="tertiary"
+ size="small"
+ class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>
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 87af3ffc52c..4234bc72f3a 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
@@ -142,6 +142,7 @@ export default {
this.setInitialState({
selectedLabels,
});
+ setTimeout(() => this.updateLabelsSetState(), 100);
},
showDropdownContents(showDropdownContents) {
this.setContentIsOnViewport(showDropdownContents);
@@ -184,7 +185,7 @@ export default {
document.removeEventListener('click', this.handleDocumentClick);
},
methods: {
- ...mapActions(['setInitialState', 'toggleDropdownContents']),
+ ...mapActions(['setInitialState', 'toggleDropdownContents', 'updateLabelsSetState']),
/**
* This method differentiates between
* dispatched actions and calls necessary method.
@@ -315,7 +316,7 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
- v-show="dropdownButtonVisible && showDropdownContents"
+ v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
:render-on-top="!contentIsOnViewport"
/>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 178be0f6da0..0c697e624ab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -20,7 +20,11 @@ export const receiveLabelsFailure = ({ commit }) => {
message: __('Error fetching labels.'),
});
};
-export const fetchLabels = ({ state, dispatch }) => {
+export const fetchLabels = ({ state, dispatch }, options) => {
+ if (state.labelsFetched && (!options || !options.refetch)) {
+ return Promise.resolve();
+ }
+
dispatch('requestLabels');
return axios
.get(state.labelsFetchPath)
@@ -46,6 +50,7 @@ export const createLabel = ({ state, dispatch }, label) => {
})
.then(({ data }) => {
if (data.id) {
+ dispatch('fetchLabels', { refetch: true });
dispatch('receiveCreateLabelSuccess');
dispatch('toggleDropdownContentsCreateView');
} else {
@@ -60,3 +65,5 @@ export const createLabel = ({ state, dispatch }, label) => {
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
+
+export const updateLabelsSetState = ({ commit }) => commit(types.UPDATE_LABELS_SET_STATE);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
index 2e044dc3b3c..f26e36031f4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
@@ -18,3 +18,5 @@ export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
+
+export const UPDATE_LABELS_SET_STATE = 'UPDATE_LABELS_SET_STATE';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 2e0a57f15dd..8853dc8b9e3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -34,15 +34,12 @@ export default {
// Iterate over every label and add a `set` prop
// to determine whether it is already a part of
// selectedLabels array.
- const selectedLabelIds = state.selectedLabels.map((label) => label.id);
state.labelsFetchInProgress = false;
- state.labels = labels.reduce((allLabels, label) => {
- allLabels.push({
- ...label,
- set: selectedLabelIds.includes(label.id),
- });
- return allLabels;
- }, []);
+ state.labelsFetched = true;
+ state.labels = labels.map((label) => ({
+ ...label,
+ set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
+ }));
},
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
@@ -79,4 +76,11 @@ export default {
}
}
},
+
+ [types.UPDATE_LABELS_SET_STATE](state) {
+ state.labels = state.labels.map((label) => ({
+ ...label,
+ set: state.selectedLabels.some((selectedLabel) => selectedLabel.id === label.id),
+ }));
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
index d66cfed4163..0185d5f88e1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
@@ -1,6 +1,7 @@
export default () => ({
// Initial Data
labels: [],
+ labelsFetched: false,
selectedLabels: [],
labelsListTitle: '',
labelsCreateTitle: '',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 1f0704f7308..6694e349b6e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -21,9 +21,29 @@ export default {
type: String,
required: true,
},
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowMultiselect: {
+ type: Boolean,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
},
computed: {
- ...mapState(['showDropdownContentsCreateView', 'labelsListTitle']),
+ ...mapState(['showDropdownContentsCreateView']),
...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() {
if (this.showDropdownContentsCreateView) {
@@ -75,6 +95,16 @@ export default {
@click="toggleDropdownContents"
/>
</div>
- <component :is="dropdownContentsView" @hideCreateView="toggleDropdownContentsCreateView" />
+ <component
+ :is="dropdownContentsView"
+ :selected-labels="selectedLabels"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ @hideCreateView="toggleDropdownContentsCreateView"
+ @closeDropdown="$emit('closeDropdown', $event)"
+ @toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index bff34743344..ffa37424c2c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,38 +1,91 @@
<script>
-import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { mapState, mapGetters, mapActions } from 'vuex';
-
+import { debounce } from 'lodash';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
-
+import { __ } from '~/locale';
+import { DropdownVariant } from './constants';
+import projectLabelsQuery from './graphql/project_labels.query.graphql';
import LabelItem from './label_item.vue';
export default {
components: {
- GlIntersectionObserver,
GlLoadingIcon,
GlSearchBoxByType,
GlLink,
LabelItem,
},
+ inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'],
+ props: {
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowMultiselect: {
+ type: Boolean,
+ required: true,
+ },
+ labelsListTitle: {
+ type: String,
+ required: true,
+ },
+ footerCreateLabelTitle: {
+ type: String,
+ required: true,
+ },
+ footerManageLabelTitle: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
searchKey: '',
+ labels: [],
currentHighlightItem: -1,
+ localSelectedLabels: [...this.selectedLabels],
};
},
+ apollo: {
+ labels: {
+ query: projectLabelsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ searchTerm: this.searchKey,
+ };
+ },
+ skip() {
+ return this.searchKey.length === 1;
+ },
+ update: (data) => data.workspace?.labels?.nodes || [],
+ async result() {
+ if (this.$refs.searchInput) {
+ await this.$nextTick();
+ this.$refs.searchInput.focusInput();
+ }
+ },
+ error() {
+ createFlash({ message: __('Error fetching labels.') });
+ },
+ },
+ },
computed: {
- ...mapState([
- 'allowLabelCreate',
- 'allowMultiselect',
- 'labelsManagePath',
- 'labels',
- 'labelsFetchInProgress',
- 'labelsListTitle',
- 'footerCreateLabelTitle',
- 'footerManageLabelTitle',
- ]),
- ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
+ isDropdownVariantSidebar() {
+ return this.variant === DropdownVariant.Sidebar;
+ },
+ isDropdownVariantEmbedded() {
+ return this.variant === DropdownVariant.Embedded;
+ },
+ labelsFetchInProgress() {
+ return this.$apollo.queries.labels.loading;
+ },
+ localSelectedLabelsIds() {
+ return this.localSelectedLabels.map((label) => label.id);
+ },
visibleLabels() {
if (this.searchKey) {
return fuzzaldrinPlus.filter(this.labels, this.searchKey, {
@@ -55,17 +108,16 @@ export default {
}
},
},
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ beforeDestroy() {
+ this.$emit('closeDropdown', this.localSelectedLabels);
+ this.debouncedSearchKeyUpdate.cancel();
+ },
methods: {
- ...mapActions([
- 'toggleDropdownContents',
- 'toggleDropdownContentsCreateView',
- 'fetchLabels',
- 'receiveLabelsSuccess',
- 'updateSelectedLabels',
- 'toggleDropdownContents',
- ]),
isLabelSelected(label) {
- return this.selectedLabelsList.includes(label.id);
+ return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id));
},
/**
* This method scrolls item from dropdown into
@@ -86,23 +138,17 @@ export default {
}
}
},
- handleComponentAppear() {
- // We can avoid putting `catch` block here
- // as failure is handled within actions.js already.
- return this.fetchLabels().then(() => {
- this.$refs.searchInput.focusInput();
- });
- },
- /**
- * We want to remove loaded labels to ensure component
- * fetches fresh set of labels every time when shown.
- */
- handleComponentDisappear() {
- this.receiveLabelsSuccess([]);
- },
- handleCreateLabelClick() {
- this.receiveLabelsSuccess([]);
- this.toggleDropdownContentsCreateView();
+ updateSelectedLabels(label) {
+ if (this.isLabelSelected(label)) {
+ this.localSelectedLabels = this.localSelectedLabels.filter(
+ ({ id }) => id !== getIdFromGraphQLId(label.id),
+ );
+ } else {
+ this.localSelectedLabels.push({
+ ...label,
+ id: getIdFromGraphQLId(label.id),
+ });
+ }
},
/**
* This method enables keyboard navigation support for
@@ -117,10 +163,10 @@ export default {
) {
this.currentHighlightItem += 1;
} else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
- this.updateSelectedLabels([this.visibleLabels[this.currentHighlightItem]]);
+ this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
this.searchKey = '';
} else if (e.keyCode === ESC_KEY_CODE) {
- this.toggleDropdownContents();
+ this.$emit('closeDropdown', this.localSelectedLabels);
}
if (e.keyCode !== ESC_KEY_CODE) {
@@ -132,68 +178,82 @@ export default {
}
},
handleLabelClick(label) {
- this.updateSelectedLabels([label]);
- if (!this.allowMultiselect) this.toggleDropdownContents();
+ this.updateSelectedLabels(label);
+ if (!this.allowMultiselect) {
+ this.$emit('closeDropdown', this.localSelectedLabels);
+ }
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
},
},
};
</script>
<template>
- <gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear">
- <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
- <div class="dropdown-input" @click.stop="() => {}">
- <gl-search-box-by-type
- ref="searchInput"
- v-model="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- />
- </div>
- <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
- <gl-loading-icon
- v-if="labelsFetchInProgress"
- class="labels-fetch-loading gl-align-items-center w-100 h-100"
- size="md"
+ <div
+ class="labels-select-contents-list js-labels-list"
+ data-testid="dropdown-wrapper"
+ @keydown="handleKeyDown"
+ >
+ <div class="dropdown-input" @click.stop="() => {}">
+ <gl-search-box-by-type
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="debouncedSearchKeyUpdate"
+ />
+ </div>
+ <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
+ <gl-loading-icon
+ v-if="labelsFetchInProgress"
+ class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
+ size="md"
+ />
+ <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list">
+ <label-item
+ v-for="(label, index) in visibleLabels"
+ :key="label.id"
+ :label="label"
+ :is-label-set="isLabelSelected(label)"
+ :highlight="index === currentHighlightItem"
+ @clickLabel="handleLabelClick(label)"
/>
- <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word">
- <label-item
- v-for="(label, index) in visibleLabels"
- :key="label.id"
- :label="label"
- :is-label-set="label.set"
- :highlight="index === currentHighlightItem"
- @clickLabel="handleLabelClick(label)"
- />
- <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
- {{ __('No matching results') }}
- </li>
- </ul>
- </div>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-footer"
- data-testid="dropdown-footer"
- >
- <ul class="list-unstyled">
- <li v-if="allowLabelCreate">
- <gl-link
- class="gl-display-flex w-100 flex-row text-break-word label-item"
- @click="handleCreateLabelClick"
- >
- {{ footerCreateLabelTitle }}
- </gl-link>
- </li>
- <li>
- <gl-link
- :href="labelsManagePath"
- class="gl-display-flex flex-row text-break-word label-item"
- >
- {{ footerManageLabelTitle }}
- </gl-link>
- </li>
- </ul>
- </div>
+ <li
+ v-show="showNoMatchingResultsMessage"
+ class="gl-p-3 gl-text-center"
+ data-testid="no-results"
+ >
+ {{ __('No matching results') }}
+ </li>
+ </ul>
+ </div>
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-footer"
+ data-testid="dropdown-footer"
+ >
+ <ul class="list-unstyled">
+ <li v-if="allowLabelCreate">
+ <gl-link
+ class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
+ data-testid="create-label-button"
+ @click="$emit('toggleDropdownContentsCreateView')"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-link>
+ </li>
+ <li>
+ <gl-link
+ :href="labelsManagePath"
+ class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
+ >
+ {{ footerManageLabelTitle }}
+ </gl-link>
+ </li>
+ </ul>
</div>
- </gl-intersection-observer>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
index b6d14965cfa..46edfa1c42a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
- variant="link"
- class="float-right js-sidebar-dropdown-toggle"
+ category="tertiary"
+ size="small"
+ class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
new file mode 100644
index 00000000000..dc39220487d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
@@ -0,0 +1,12 @@
+query projectLabels($fullPath: ID!, $searchTerm: String) {
+ workspace: project(fullPath: $fullPath) {
+ labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 87f36a5bb72..0499dfe468f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -197,23 +197,6 @@ export default {
methods: {
...mapActions(['setInitialState', 'toggleDropdownContents']),
/**
- * This method differentiates between
- * dispatched actions and calls necessary method.
- */
- handleVuexActionDispatch(action, state) {
- if (
- action.type === 'toggleDropdownContents' &&
- !state.showDropdownButton &&
- !state.showDropdownContents
- ) {
- let filterFn = (label) => label.touched;
- if (this.isDropdownVariantEmbedded) {
- filterFn = (label) => label.set;
- }
- this.handleDropdownClose(state.labels.filter(filterFn));
- }
- },
- /**
* This method stores a mousedown event's target.
* Required by the click listener because the click
* event itself has no reference to this element.
@@ -276,6 +259,9 @@ export default {
handleDropdownClose(labels) {
// Only emit label updates if there are any labels to update
// on UI.
+ if (this.showDropdownContents) {
+ this.toggleDropdownContents();
+ }
if (labels.length) this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose');
},
@@ -330,10 +316,16 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
- v-show="dropdownButtonVisible && showDropdownContents"
+ v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport"
:labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ @closeDropdown="handleDropdownClose"
/>
</template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
@@ -341,7 +333,13 @@ export default {
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
:render-on-top="!contentIsOnViewport"
+ :selected-labels="selectedLabels"
+ @closeDropdown="handleDropdownClose"
/>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
index 935f020f559..b3d4a204a81 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
@@ -1,6 +1,3 @@
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
import * as types from './mutation_types';
export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
@@ -11,24 +8,5 @@ export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDO
export const toggleDropdownContentsCreateView = ({ commit }) =>
commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW);
-export const requestLabels = ({ commit }) => commit(types.REQUEST_LABELS);
-export const receiveLabelsSuccess = ({ commit }, labels) =>
- commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
-export const receiveLabelsFailure = ({ commit }) => {
- commit(types.RECEIVE_SET_LABELS_FAILURE);
- createFlash({
- message: __('Error fetching labels.'),
- });
-};
-export const fetchLabels = ({ state, dispatch }) => {
- dispatch('requestLabels');
- return axios
- .get(state.labelsFetchPath)
- .then(({ data }) => {
- dispatch('receiveLabelsSuccess', data);
- })
- .catch(() => dispatch('receiveLabelsFailure'));
-};
-
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
index b8da7a90b36..bd71c3b85f1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
@@ -1,13 +1,5 @@
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
-export const REQUEST_LABELS = 'REQUEST_LABELS';
-export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
-export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
-
-export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
-export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
-export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
-
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
index 1c03d95f37b..45ec4d7ae04 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
@@ -26,27 +26,6 @@ export default {
[types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) {
state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView;
},
-
- [types.REQUEST_LABELS](state) {
- state.labelsFetchInProgress = true;
- },
- [types.RECEIVE_SET_LABELS_SUCCESS](state, labels) {
- // Iterate over every label and add a `set` prop
- // to determine whether it is already a part of
- // selectedLabels array.
- const selectedLabelIds = state.selectedLabels.map((label) => label.id);
- state.labelsFetchInProgress = false;
- state.labels = labels.reduce((allLabels, label) => {
- allLabels.push({
- ...label,
- set: selectedLabelIds.includes(label.id),
- });
- return allLabels;
- }, []);
- },
- [types.RECEIVE_SET_LABELS_FAILURE](state) {
- state.labelsFetchInProgress = false;
- },
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
index e6229cf0a93..cdc7422c7df 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { todoLabel } from './utils';
+import { todoLabel, updateGlobalTodoCount } from './utils';
export default {
components: {
@@ -19,23 +19,11 @@ export default {
},
},
methods: {
- updateGlobalTodoCount(additionalTodoCount) {
- const countContainer = document.querySelector('.js-todos-count');
- if (countContainer === null) return;
- const currentCount = parseInt(countContainer.innerText, 10);
- const todoToggleEvent = new CustomEvent('todo:toggle', {
- detail: {
- count: Math.max(currentCount + additionalTodoCount, 0),
- },
- });
-
- document.dispatchEvent(todoToggleEvent);
- },
incrementGlobalTodoCount() {
- this.updateGlobalTodoCount(1);
+ updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
- this.updateGlobalTodoCount(-1);
+ updateGlobalTodoCount(-1);
},
onToggle(event) {
if (this.isTodo) {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
index 59e72a2ffe3..098ab72dfb5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
@@ -3,3 +3,19 @@ import { __ } from '~/locale';
export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do');
};
+
+export const updateGlobalTodoCount = (additionalTodoCount) => {
+ const countContainer = document.querySelector('.js-todos-count');
+
+ if (countContainer === null) return;
+
+ const currentCount = parseInt(countContainer.innerText, 10);
+
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+};
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index 55e2a786c8f..04423aac651 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -30,6 +30,11 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
linkHref: {
type: String,
required: false,
@@ -91,6 +96,7 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
>
<slot></slot> </user-avatar-image
><span
diff --git a/app/assets/javascripts/vue_shared/components/user_date.vue b/app/assets/javascripts/vue_shared/components/user_date.vue
index 38dddbf72c2..33531cc3278 100644
--- a/app/assets/javascripts/vue_shared/components/user_date.vue
+++ b/app/assets/javascripts/vue_shared/components/user_date.vue
@@ -1,7 +1,7 @@
<script>
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
-import { SHORT_DATE_FORMAT } from '../constants';
+import { SHORT_DATE_FORMAT, DATE_FORMATS } from '../constants';
export default {
props: {
@@ -10,6 +10,12 @@ export default {
required: false,
default: null,
},
+ dateFormat: {
+ type: String,
+ required: false,
+ default: SHORT_DATE_FORMAT,
+ validator: (dateFormat) => DATE_FORMATS.includes(dateFormat),
+ },
},
computed: {
formattedDate() {
@@ -17,7 +23,7 @@ export default {
if (date === null) {
return __('Never');
}
- return formatDate(new Date(date), SHORT_DATE_FORMAT);
+ return formatDate(new Date(date), this.dateFormat);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 5ba7c107c12..df0981aea7a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -59,11 +59,21 @@ export default {
required: false,
default: '',
},
+ webIdeText: {
+ type: String,
+ required: false,
+ default: '',
+ },
gitpodUrl: {
type: String,
required: false,
default: '',
},
+ gitpodText: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -99,6 +109,17 @@ export default {
...handleOptions,
};
},
+ webIdeActionText() {
+ if (this.webIdeText) {
+ return this.webIdeText;
+ } else if (this.isBlob) {
+ return __('Edit in Web IDE');
+ } else if (this.isFork) {
+ return __('Edit fork in Web IDE');
+ }
+
+ return __('Web IDE');
+ },
webIdeAction() {
if (!this.showWebIdeButton) {
return null;
@@ -111,17 +132,9 @@ export default {
}
: { href: this.webIdeUrl };
- let text = __('Web IDE');
-
- if (this.isBlob) {
- text = __('Edit in Web IDE');
- } else if (this.isFork) {
- text = __('Edit fork in Web IDE');
- }
-
return {
key: KEY_WEB_IDE,
- text,
+ text: this.webIdeActionText,
secondaryText: __('Quickly and easily edit multiple files in your project.'),
tooltip: '',
attrs: {
@@ -132,6 +145,9 @@ export default {
...handleOptions,
};
},
+ gitpodActionText() {
+ return this.gitpodText || __('Gitpod');
+ },
gitpodAction() {
if (!this.showGitpodButton) {
return null;
@@ -145,7 +161,7 @@ export default {
return {
key: KEY_GITPOD,
- text: __('Gitpod'),
+ text: this.gitpodActionText,
secondaryText,
tooltip: secondaryText,
attrs: {
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 9a5ad195de9..33fac5ebdbb 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -10,6 +10,10 @@ export const FILE_SYMLINK_MODE = '120000';
export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
+export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
+
+export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
+
export const timeRanges = [
{
label: __('30 minutes'),
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
index 1b20ae57563..5cd2018bb8c 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -1,12 +1,12 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import Vue from 'vue';
import Tracking from '~/tracking';
export default {
directives: {
SafeHtml,
},
+ mixins: [Tracking.mixin()],
props: {
title: {
type: String,
@@ -17,16 +17,6 @@ export default {
required: true,
},
},
- created() {
- const trackingMixin = Tracking.mixin();
- const trackingInstance = new Vue({
- ...trackingMixin,
- render() {
- return null;
- },
- });
- this.track = trackingInstance.track;
- },
};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/provider.js b/app/assets/javascripts/vue_shared/security_configuration/provider.js
index ef96b443da8..fa23669b615 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/provider.js
+++ b/app/assets/javascripts/vue_shared/security_configuration/provider.js
@@ -5,5 +5,5 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index f3dd26b02cb..3a4453bc7ae 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -3,7 +3,7 @@ import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/securi
import createFlash from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
-import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql';
+import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils';
export default {
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
index 4178c5d1170..28618cb96a3 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -32,6 +32,11 @@ export default {
default: '',
},
},
+ computed: {
+ showDropdown() {
+ return this.loading || this.artifacts.length > 0;
+ },
+ },
methods: {
artifactText({ name }) {
return sprintf(s__('SecurityReports|Download %{artifactName}'), {
@@ -44,6 +49,7 @@ export default {
<template>
<gl-dropdown
+ v-if="showDropdown"
v-gl-tooltip
:text="text"
:title="title"
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql
new file mode 100644
index 00000000000..ae77a2ce5e4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql
@@ -0,0 +1,13 @@
+fragment JobArtifacts on Pipeline {
+ jobs(securityReportTypes: $reportTypes) {
+ nodes {
+ name
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
index 4ce13827da2..4ce13827da2 100644
--- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql
diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
new file mode 100644
index 00000000000..b5858ab012b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/job_artifacts.fragment.graphql"
+
+query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
+ project(fullPath: $projectPath) {
+ pipeline(iid: $iid) {
+ id
+ ...JobArtifacts
+ }
+ }
+}
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
deleted file mode 100644
index c7e9fa16418..00000000000
--- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-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 3e0310e173e..ad40ea6a964 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,7 +13,7 @@ import {
REPORT_TYPE_SECRET_DETECTION,
reportTypeToSecurityReportTypeEnum,
} from './constants';
-import securityReportMergeRequestDownloadPathsQuery from './queries/security_report_merge_request_download_paths.query.graphql';
+import securityReportMergeRequestDownloadPathsQuery from './graphql/queries/security_report_merge_request_download_paths.query.graphql';
import store from './store';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import { extractSecurityReportArtifactsFromMergeRequest } from './utils';
diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js
index c3f24a7e52f..0add91c402e 100644
--- a/app/assets/javascripts/vue_shared/security_reports/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/utils.js
@@ -14,7 +14,7 @@ const addReportTypeIfExists = (acc, reportTypes, reportType, getName, downloadPa
}
};
-const extractSecurityReportArtifacts = (reportTypes, jobs) => {
+export const extractSecurityReportArtifacts = (reportTypes, jobs) => {
return jobs.reduce((acc, job) => {
const artifacts = job.artifacts?.nodes ?? [];
diff --git a/app/assets/stylesheets/_jh/application_jh.scss b/app/assets/stylesheets/_jh/application_jh.scss
new file mode 100644
index 00000000000..9d9918d5bbd
--- /dev/null
+++ b/app/assets/stylesheets/_jh/application_jh.scss
@@ -0,0 +1,5 @@
+/*
+ This is a noop-file. In JH:
+ jh/app/assets/stylesheets/_jh/application_jh.scss
+ will take precedence over it and import more styles
+ */
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 9ef1b58ed24..40228b93e01 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -24,6 +24,9 @@
// EE-only stylesheets
@import 'application_ee';
+// JH-only stylesheets
+@import 'application_jh';
+
/* print styles */
@media print {
@import 'print';
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
new file mode 100644
index 00000000000..64abf5574fa
--- /dev/null
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -0,0 +1,56 @@
+.ProseMirror {
+ td,
+ th,
+ li {
+ :only-child {
+ margin-bottom: 0 !important;
+ }
+ }
+
+ ul[data-type='taskList'] {
+ list-style: none;
+ padding: 0;
+
+ li {
+ margin: 0 !important;
+ }
+ }
+
+ [data-type='taskList'] {
+ p {
+ margin-bottom: 0;
+ }
+
+ li {
+ > label,
+ > div {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+ > label {
+ padding: $gl-spacing-scale-1 $gl-spacing-scale-3 0 0;
+ margin: 0;
+ }
+ }
+ }
+}
+
+.table-creator-grid-item {
+ box-shadow: inset 0 0 0 $gl-spacing-scale-2 $white,
+ inset $gl-spacing-scale-1 $gl-spacing-scale-1 0 #{$gl-spacing-scale-2 * 3 / 4} $gray-100,
+ inset #{-$gl-spacing-scale-1} #{-$gl-spacing-scale-1} 0 #{$gl-spacing-scale-2 * 3 / 4} $gray-100 !important;
+
+ &.active {
+ box-shadow: inset 0 0 0 $gl-spacing-scale-2 $white,
+ inset $gl-spacing-scale-1 $gl-spacing-scale-1 0 $gl-spacing-scale-2 $blue-500,
+ inset #{-$gl-spacing-scale-1} #{-$gl-spacing-scale-1} 0 $gl-spacing-scale-2 $blue-500 !important;
+ }
+}
+
+.table-dropdown .dropdown-menu {
+ @include gl-min-w-0;
+ @include gl-w-auto;
+
+ @include gl-white-space-nowrap;
+}
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
index 7af97505749..4437b5b673d 100644
--- a/app/assets/stylesheets/components/whats_new.scss
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -36,7 +36,15 @@
}
.with-performance-bar .whats-new-drawer {
- margin-top: calc(#{$performance-bar-height} + #{$header-height});
+ margin-top: $performance-bar-height + $header-height;
+}
+
+.with-system-header .whats-new-drawer {
+ margin-top: $system-header-height + $header-height;
+}
+
+.with-performance-bar.with-system-header .whats-new-drawer {
+ margin-top: $performance-bar-height + $system-header-height + $header-height;
}
.gl-badge.whats-new-item-badge {
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index a93c70c75d3..fa235f72e35 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -98,7 +98,6 @@
}
.note-action-button,
-.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 10481294df5..264373451d5 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -6,7 +6,7 @@
.nothing-here-block {
text-align: center;
- padding: 20px;
+ padding: 16px;
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
font-size: 14px;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index f8b1735207c..61a20c7a8fd 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -839,6 +839,18 @@ table.code {
}
}
+.commits-container {
+ .diff-files-changed {
+ @include media-breakpoint-up(sm) {
+ top: $header-height;
+
+ .with-performance-bar & {
+ top: $header-height + $performance-bar-height;
+ }
+ }
+ }
+}
+
.diff-files-changed {
.inline-parallel-buttons {
position: relative;
@@ -854,12 +866,12 @@ table.code {
@include media-breakpoint-up(sm) {
position: -webkit-sticky;
position: sticky;
- top: $header-height;
+ top: $header-height + $mr-tabs-height;
background-color: $white;
z-index: 200;
.with-performance-bar & {
- top: $header-height + $performance-bar-height;
+ top: $header-height + $mr-tabs-height + $performance-bar-height;
}
&.is-stuck {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 144a396ea65..b05fbfaae6c 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -50,12 +50,6 @@
width: 100%;
}
- &.frequent-items-dropdown-menu {
- padding: 0;
- overflow-y: initial;
- max-height: initial;
- }
-
// `GlDropdown` specifies the `max-height` of `.gl-new-dropdown-inner`
// as `$dropdown-max-height`, but the `max-height` rule above forces
// the parent `.dropdown-menu` to be _slightly_ too small because of
@@ -834,61 +828,11 @@
}
}
-header.header-content .dropdown-menu.frequent-items-dropdown-menu {
- padding: 0;
-}
-
.frequent-items-dropdown-container {
display: flex;
flex-direction: row;
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 {
@include gl-pt-3;
}
@@ -897,11 +841,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
color: $almost-black;
}
- .frequent-items-dropdown-sidebar {
- width: 30%;
- border-right: 1px solid $border-color;
- }
-
.frequent-items-dropdown-content {
position: relative;
width: 70%;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 5ad7ceecb2b..df78543f96d 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -15,6 +15,16 @@
border-top: 0;
border-radius: $border-radius-default;
+ &.file-holder-top-border {
+ border-top: 1px solid $border-color;
+
+ .file-title {
+ // Prevents the top border getting clipped by the background
+ border-top-left-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+ }
+ }
+
&.file-holder-no-border {
border: 0;
}
@@ -136,6 +146,13 @@
* Blame file
*/
&.blame {
+ //
+ // IMPORTANT PERFORMANCE OPTIMIZATION
+ //
+ // When viewinng a blame with many commits a lot of content is rendered on the page.
+ // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
+ content-visibility: auto;
+
table {
border: 0;
margin: 0;
@@ -150,6 +167,12 @@
}
td {
+ //
+ // IMPORTANT PERFORMANCE OPTIMIZATION
+ //
+ // When viewinng a blame with many commits a lot of content is rendered on the page.
+ // The line below ensures that we only render what is visible to the user, thus reducing TBT in the browser.
+ content-visibility: auto;
border-top: 0;
border-bottom: 0;
@@ -509,6 +532,24 @@ span.idiff {
}
}
+.version-link {
+ @include gl-display-inline-block;
+ @include gl-align-self-center;
+ @include gl-mt-2;
+ @include gl-w-5;
+ @include gl-h-5;
+ @include gl-float-left;
+ background-color: $gray-400;
+ mask-image: asset_url('icons-stacked.svg#doc-versions');
+ mask-repeat: no-repeat;
+ mask-size: cover;
+ mask-position: center;
+
+ &:hover {
+ background-color: $black;
+ }
+}
+
//
// IMPORTANT PERFORMANCE OPTIMIZATION BELOW
//
@@ -530,3 +571,18 @@ span.idiff {
// will always be expanded to the maximum needed width.
.blob-viewer[data-loading] .file-content.code .line:nth-of-type(1n+70):not(:last-of-type),
.blob-viewer[data-loading] .file-content.code .file-line-num:nth-of-type(1n+70):not(:last-of-type) {display: none !important;}
+
+.blob-viewer[data-loading] .file-content.code .line:nth-of-type(69):not(:last-of-type),
+.blob-viewer[data-loading] .file-content.code .file-line-num:nth-of-type(69):not(:last-of-type) {
+ &::after {
+ @include gl-display-block;
+ @include gl-font-weight-bold;
+ content: '\2026';
+ }
+}
+
+.blob-viewer[data-loading] .file-content.code .line:nth-of-type(69):not(:last-of-type) {
+ &::after {
+ @include gl-text-center;
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 65d914e47cf..ae46ff33ec0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -27,17 +27,6 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
display: none;
}
- .menu-expanded {
- .more-icon {
- display: none;
- }
-
- .close-icon {
- display: block;
- margin: auto;
- }
- }
-
.header-content {
width: 100%;
display: flex;
@@ -103,18 +92,6 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
.navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
-
- &.menu-expanded {
- @include media-breakpoint-down(xs) {
- .hide-when-menu-expanded {
- display: none;
- }
-
- .navbar-collapse {
- display: flex;
- }
- }
- }
}
.navbar-collapse {
@@ -673,19 +650,30 @@ $top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important
}
.top-nav-responsive-open {
- .hide-when-top-nav-responsive-open {
- @include media-breakpoint-down(xs) {
+ .more-icon {
+ display: none;
+ }
+
+ .close-icon {
+ display: block;
+ margin: auto;
+ }
+
+ @include media-breakpoint-down(xs) {
+ .navbar-collapse {
+ display: flex;
+ }
+
+ .hide-when-top-nav-responsive-open {
display: none !important;
}
- }
- .top-nav-responsive {
- @include media-breakpoint-down(xs) {
+ .top-nav-responsive {
@include gl-display-block;
}
- }
- .navbar-gitlab .header-content .title-container {
- flex: 0;
+ .navbar-gitlab .header-content .title-container {
+ flex: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 2a97009e605..7315bce1ed9 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -83,6 +83,7 @@
li.md-header-toolbar {
margin-left: auto;
display: none;
+ padding-bottom: $gl-padding-8;
&.active {
display: block;
@@ -92,8 +93,8 @@
display: flex;
justify-content: center;
width: 100%;
- padding-top: $gl-padding-top;
- padding-bottom: $gl-padding-top;
+ flex-wrap: wrap;
+ margin-top: $gl-padding-8;
}
}
}
@@ -131,36 +132,6 @@
width: 100%;
}
-.toolbar-btn {
- float: left;
- padding: 0 7px;
- background: transparent;
- border: 0;
- outline: 0;
-
- svg {
- width: 14px;
- height: 14px;
- vertical-align: middle;
- fill: $gl-text-color-secondary;
- }
-
- &:hover,
- &:focus {
- svg {
- fill: $blue-600;
- }
- }
-}
-
-.toolbar-fullscreen-btn {
- margin-right: -5px;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
-}
-
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f904ef11f5b..06eebb95438 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -14,7 +14,6 @@
a,
button {
padding: $gl-padding-8;
- padding-bottom: $gl-padding-8 + 1;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index e35feb8c62d..6b3201ba2b0 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -237,3 +237,7 @@
@include side-panel-toggle;
border-bottom: 1px solid $border-color;
}
+
+.edit-link {
+ margin-right: -$gl-spacing-scale-2;
+}
diff --git a/app/assets/stylesheets/framework/source_editor.scss b/app/assets/stylesheets/framework/source_editor.scss
index a967d9a71f1..8b694b9be05 100644
--- a/app/assets/stylesheets/framework/source_editor.scss
+++ b/app/assets/stylesheets/framework/source_editor.scss
@@ -25,6 +25,21 @@
height: 500px;
}
+.source-editor-preview {
+ @include gl-display-flex;
+
+ .md {
+ @include gl-overflow-scroll;
+ @include gl-px-6;
+ @include gl-py-4;
+ @include gl-w-full;
+ }
+
+ .gl-source-editor {
+ @include gl-order-n1;
+ }
+}
+
.monaco-editor.gl-source-editor {
.margin-view-overlays {
.line-numbers {
diff --git a/app/assets/stylesheets/lazy_bundles/select2_overrides.scss b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
index 272f94176d0..3e5271f84d5 100644
--- a/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
+++ b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
@@ -14,7 +14,7 @@
.select2-choice {
background: var(--white, $white);
color: var(--gl-text-color, $gl-text-color);
- border-color: var(--border-color, $border-color);
+ border-color: var(--gray-400, $gray-400);
height: 34px;
padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size;
@@ -41,7 +41,7 @@
}
&:hover {
- border-color: var(--gray-200, $gray-200);
+ border-color: var(--gray-400, $gray-400);
color: var(--gl-text-color, $gl-text-color);
}
}
@@ -99,7 +99,7 @@
background: var(--white, $white);
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $gl-border-radius-base;
- border: 1px solid var(--border-color, $border-color);
+ border: 1px solid var(--gray-400, $gray-400);
min-width: 175px;
color: var(--gl-text-color, $gl-text-color);
z-index: 999;
@@ -118,7 +118,7 @@
}
.select2-drop.select2-drop-above.select2-drop-active {
- border-top: 1px solid var(--border-color, $border-color);
+ border-top: 1px solid var(--gray-400, $gray-400);
margin-top: -6px;
}
@@ -132,7 +132,7 @@
.select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice {
- border-color: var(--border-color, $border-color);
+ border-color: var(--gray-400, $gray-400);
outline: 0;
}
}
@@ -140,7 +140,7 @@
.select2-container-multi {
.select2-choices {
border-radius: $border-radius-default;
- border-color: var(--border-color, $border-color);
+ border-color: var(--gray-400, $gray-400);
background: none;
.select2-search-field input {
@@ -153,7 +153,7 @@
.select2-search-choice {
margin: 5px 0 0 8px;
box-shadow: none;
- border-color: var(--border-color, $border-color);
+ border-color: var(--gray-400, $gray-400);
color: var(--gl-text-color, $gl-text-color);
line-height: 15px;
background-color: var(--gray-50, $gray-50);
@@ -167,7 +167,7 @@
}
&.select2-search-choice-focus {
- border-color: var(--gl-text-color, $gl-text-color);
+ border-color: var(--gray-400, $gray-400);
}
}
}
@@ -197,7 +197,7 @@
background-origin: content-box;
background-repeat: no-repeat;
background-position: right 0 bottom 0 !important;
- border: 1px solid var(--border-color, $border-color);
+ border: 1px solid var(--gray-400, $gray-400);
border-radius: $border-radius-default;
line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 428bd90ddd7..10183f774b1 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -153,16 +153,16 @@
// pseudo-element that is the same size as our element, then
// animate opacity/transform to give a soothing single pulse
.board-column-highlighted::after {
+ @include gl-focus;
content: '';
position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
+ top: -1px;
+ bottom: -1px;
+ left: -1px;
+ right: -1px;
pointer-events: none;
opacity: 0;
- z-index: -1;
- box-shadow: 0 0 6px 3px $blue-200;
+ border-radius: $border-radius-default;
animation-name: board-column-flash-border;
animation-duration: 1.2s;
animation-fill-mode: forwards;
@@ -173,18 +173,11 @@
0%,
100% {
opacity: 0;
- transform: scale(0.98);
}
25%,
75% {
opacity: 1;
- transform: scale(0.99);
- }
-
- 50% {
- opacity: 1;
- transform: scale(1);
}
}
@@ -476,6 +469,10 @@
.gl-drawer-header {
align-items: flex-start;
}
+
+ .labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
+ width: auto;
+ }
}
.board-header-collapsed-info-icon:hover {
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index 525481638f3..c74b5460e1a 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -22,16 +22,11 @@
.import-entities-target-select {
&.disabled {
- .import-entities-target-select-separator,
- .select2-container.select2-container-disabled .select2-choice {
+ .import-entities-target-select-separator {
color: var(--gray-400, $gray-400);
border-color: var(--gray-100, $gray-100);
background-color: var(--gray-10, $gray-10);
}
-
- .select2-container.select2-container-disabled .select2-choice .select2-arrow {
- background-color: var(--gray-10, $gray-10);
- }
}
.import-entities-target-select-separator {
@@ -39,20 +34,6 @@
background-color: var(--gray-10, $gray-10);
}
- .select2-container {
- > .select2-choice {
- .select2-arrow {
- background-color: var(--white, $white);
- }
-
- border-color: var(--gray-200, $gray-200);
- color: var(--gray-900, $gray-900) !important;
- background-color: var(--white, $white) !important;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
- }
-
.gl-form-input {
box-shadow: inset 0 0 0 1px var(--gray-200, $gray-200);
}
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 5525ad66e42..1e6567189be 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -1,5 +1,6 @@
@import 'mixins_and_variables_and_functions';
@import 'highlight.js/scss/a11y-light';
+@import 'components/content_editor';
.title .edit-wiki-header {
width: 780px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index c8da025131d..33d00027404 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -154,21 +154,6 @@
}
}
-/*
- * Last push widget
- */
-.event-last-push {
- width: 100%;
- display: flex;
- align-items: center;
-
- .event-last-push-text {
- @include str-truncated(100%);
- font-size: 13px;
- margin-right: $gl-padding;
- }
-}
-
@include media-breakpoint-down(xs) {
.event-item {
padding-left: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 8807ab5e597..071a5be073f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -1003,10 +1003,10 @@ $tabs-holder-z-index: 250;
.mr-compare {
.diff-file .file-title-flex-parent {
- top: $header-height + 51px;
+ top: $header-height + $mr-tabs-height + 36px;
.with-performance-bar & {
- top: $performance-bar-height + $header-height + 51px;
+ top: $performance-bar-height + $header-height + $mr-tabs-height + 36px;
}
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c025d8569a7..34a03a07405 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -54,7 +54,7 @@
.common-note-form {
.md-area {
- padding: $gl-padding-top $gl-padding;
+ padding: $gl-padding-8 $gl-padding;
border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
@@ -295,12 +295,6 @@ table {
}
}
}
-
- @include media-breakpoint-down(xs) {
- .btn {
- width: 100%;
- }
- }
}
.discussion-reply-holder {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4a866489806..17bc40b4dec 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -649,7 +649,7 @@ $system-note-svg-size: 16px;
@include media-breakpoint-down(xs) {
width: 100%;
- margin: $gl-padding-8 0;
+ margin: 0 0 $gl-padding-8;
}
.btn-group > .discussion-next-btn {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 5fbb2e6443f..8f5de73365b 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -13,10 +13,6 @@ $search-sidebar-max-width: 300px;
border-bottom: 0;
}
}
-
- .blob-result {
- margin: 5px 0;
- }
}
.search-sidebar {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index a497f56f3b8..a90751f772e 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -336,9 +336,6 @@ h1 {
.d-none {
display: none !important;
}
-.d-inline-block {
- display: inline-block !important;
-}
.d-block {
display: block !important;
}
@@ -363,11 +360,6 @@ h1 {
display: block !important;
}
}
-@media (min-width: 1200px) {
- .d-xl-block {
- display: block !important;
- }
-}
.sr-only {
position: absolute;
width: 1px;
@@ -488,7 +480,7 @@ body {
.btn:active,
.btn.active {
background-color: #444;
- border-color: #fafafa;
+ border-color: #4f4f4f;
color: #fafafa;
}
.btn svg {
@@ -1407,10 +1399,6 @@ svg.s16 {
width: 16px;
height: 16px;
}
-svg.s18 {
- width: 18px;
- height: 18px;
-}
svg.s32 {
width: 32px;
height: 32px;
@@ -1499,12 +1487,6 @@ svg.s16 {
height: 16px;
margin-right: 8px;
}
-.avatar.s18,
-.avatar-container.s18 {
- width: 18px;
- height: 18px;
- margin-right: 8px;
-}
.avatar.s32,
.avatar-container.s32 {
width: 32px;
@@ -1583,9 +1565,6 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
-.rect-avatar.s18 {
- border-radius: 2px;
-}
.rect-avatar.s32,
.nav-sidebar-inner-scroll
> div.context-header
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 76d10300307..0b2d34b6f5d 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -317,9 +317,6 @@ h1 {
.d-none {
display: none !important;
}
-.d-inline-block {
- display: inline-block !important;
-}
.d-block {
display: block !important;
}
@@ -344,11 +341,6 @@ h1 {
display: block !important;
}
}
-@media (min-width: 1200px) {
- .d-xl-block {
- display: block !important;
- }
-}
.sr-only {
position: absolute;
width: 1px;
@@ -1388,10 +1380,6 @@ svg.s16 {
width: 16px;
height: 16px;
}
-svg.s18 {
- width: 18px;
- height: 18px;
-}
svg.s32 {
width: 32px;
height: 32px;
@@ -1480,12 +1468,6 @@ svg.s16 {
height: 16px;
margin-right: 8px;
}
-.avatar.s18,
-.avatar-container.s18 {
- width: 18px;
- height: 18px;
- margin-right: 8px;
-}
.avatar.s32,
.avatar-container.s32 {
width: 32px;
@@ -1564,9 +1546,6 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
-.rect-avatar.s18 {
- border-radius: 2px;
-}
.rect-avatar.s32,
.nav-sidebar-inner-scroll
> div.context-header
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index ea7aaaa8ec8..8e1438eaf8a 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -184,10 +184,24 @@ body.gl-dark {
}
}
}
+
+ .gl-datepicker-theme {
+ .pika-prev,
+ .pika-next {
+ filter: invert(0.9);
+ }
+
+ .is-selected > .pika-button {
+ color: $gray-900;
+ }
+
+ :not(.is-selected) > .pika-button:hover {
+ background-color: $gray-200;
+ }
+ }
}
-$border-white-light: $gray-900;
-$border-white-normal: $gray-900;
+$border-white-normal: $border-color;
$body-bg: $gray-10;
$input-bg: $white;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 10334d771b8..ccad503c1ed 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -87,12 +87,6 @@
padding-bottom: $gl-spacing-scale-8;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1495
-.gl-py-13 {
- padding-top: $gl-spacing-scale-13;
- padding-bottom: $gl-spacing-scale-13;
-}
-
.gl-transition-property-stroke-opacity {
transition-property: stroke-opacity;
}
@@ -203,6 +197,10 @@
margin-bottom: -$gl-spacing-scale-3;
}
+.gl-mr-n2 {
+ margin-right: -$gl-spacing-scale-2;
+}
+
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408
$gl-line-height-42: px-to-rem(42px);
@@ -228,6 +226,10 @@ $gl-line-height-42: px-to-rem(42px);
max-height: none !important;
}
+.gl-max-w-50p {
+ max-width: 50%;
+}
+
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-popover {
.popover-header {
@@ -247,3 +249,12 @@ $gl-line-height-42: px-to-rem(42px);
.gl-min-w-8 {
min-width: $gl-spacing-scale-8;
}
+
+// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1526
+.gl-opacity-6 {
+ opacity: 0.6;
+}
+
+.gl-opacity-7 {
+ opacity: 0.7;
+}
diff --git a/app/controllers/admin/clusters/applications_controller.rb b/app/controllers/admin/clusters/applications_controller.rb
deleted file mode 100644
index 7400cc16175..00000000000
--- a/app/controllers/admin/clusters/applications_controller.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::Clusters::ApplicationsController < Clusters::ApplicationsController
- include EnforcesAdminAuthentication
-
- private
-
- def clusterable
- @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user)
- end
-end
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index 76c1c46e0e8..a3eb24b9b6f 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -2,19 +2,26 @@
class Admin::IntegrationsController < Admin::ApplicationController
include IntegrationsActions
- include IntegrationsHelper
before_action :not_found, unless: -> { instance_level_integrations? }
feature_category :integrations
+ def overrides
+ respond_to do |format|
+ format.json do
+ projects = Project.with_active_integration(integration.class).merge(::Integration.with_custom_settings)
+ serializer = ::Integrations::ProjectSerializer.new.with_pagination(request, response)
+
+ render json: serializer.represent(projects)
+ end
+ format.html { render 'shared/integrations/overrides' }
+ end
+ end
+
private
def find_or_initialize_non_project_specific_integration(name)
Integration.find_or_initialize_non_project_specific_integration(name, instance: true)
end
-
- def scoped_edit_integration_path(integration)
- edit_admin_application_settings_integration_path(integration)
- end
end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index 7761ffaac84..3b408de5f01 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -3,7 +3,7 @@
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
- feature_category :continuous_integration
+ feature_category :runner
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index d1c91d9617f..8c74352a179 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -4,19 +4,11 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
- before_action only: [:index] do
- push_frontend_feature_flag(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
- end
feature_category :runner
- NUMBER_OF_RUNNERS_PER_PAGE = 30
-
def index
- finder = Ci::RunnersFinder.new(current_user: current_user, params: params)
- @runners = finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
@active_runners_count = Ci::Runner.online.count
- @sort = finder.sort_key
end
def show
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
deleted file mode 100644
index d34773ee4dc..00000000000
--- a/app/controllers/admin/services_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::ServicesController < Admin::ApplicationController
- include Integrations::Params
-
- before_action :integration, only: [:edit, :update]
- before_action :disable_query_limiting, only: [:index]
-
- feature_category :integrations
-
- def index
- @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 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 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'
- else
- render :edit
- end
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- 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
- Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/220357')
- end
-end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 145b4d10b16..3801906635f 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -26,9 +26,10 @@ class Admin::UsersController < Admin::ApplicationController
def show
end
+ # rubocop: disable CodeReuse/ActiveRecord
def projects
- @personal_projects = user.personal_projects
- @joined_projects = user.projects.joined(@user)
+ @personal_projects = user.personal_projects.includes(:topics)
+ @joined_projects = user.projects.joined(@user).includes(:topics)
end
def keys
@@ -136,7 +137,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def unban
- if update_user { |user| user.activate }
+ result = Users::UnbanService.new(current_user).execute(user)
+
+ if result[:status] == :success
redirect_back_or_admin_user(notice: _("Successfully unbanned"))
else
redirect_back_or_admin_user(alert: _("Error occurred. User was not unbanned"))
@@ -145,7 +148,7 @@ class Admin::UsersController < Admin::ApplicationController
def unlock
if update_user { |user| user.unlock_access! }
- redirect_back_or_admin_user(alert: _("Successfully unlocked"))
+ redirect_back_or_admin_user(notice: _("Successfully unlocked"))
else
redirect_back_or_admin_user(alert: _("Error occurred. User was not unlocked"))
end
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
deleted file mode 100644
index 91003e9580d..00000000000
--- a/app/controllers/clusters/applications_controller.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-class Clusters::ApplicationsController < Clusters::BaseController
- before_action :cluster
- before_action :authorize_create_cluster!, only: [:create]
- before_action :authorize_update_cluster!, only: [:update]
- before_action :authorize_admin_cluster!, only: [:destroy]
-
- def create
- request_handler do
- Clusters::Applications::CreateService
- .new(@cluster, current_user, cluster_application_params)
- .execute(request)
- end
- end
-
- def update
- request_handler do
- Clusters::Applications::UpdateService
- .new(@cluster, current_user, cluster_application_params)
- .execute(request)
- end
- end
-
- def destroy
- request_handler do
- Clusters::Applications::DestroyService
- .new(@cluster, current_user, cluster_application_destroy_params)
- .execute(request)
- end
- end
-
- private
-
- def request_handler
- yield
-
- head :no_content
- rescue Clusters::Applications::BaseService::InvalidApplicationError
- render_404
- rescue StandardError
- head :bad_request
- end
-
- def cluster
- @cluster ||= clusterable.clusters.find(params[:id]) || render_404
- end
-
- def cluster_application_params
- params.permit(:application, :hostname, :pages_domain_id, :email, :stack, :host, :port, :protocol)
- end
-
- def cluster_application_destroy_params
- params.permit(:application)
- end
-end
diff --git a/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb
new file mode 100644
index 00000000000..eebc40f33f4
--- /dev/null
+++ b/app/controllers/concerns/analytics/cycle_analytics/stage_actions.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module StageActions
+ include Gitlab::Utils::StrongMemoize
+ extend ActiveSupport::Concern
+
+ included do
+ include CycleAnalyticsParams
+
+ before_action :validate_params, only: %i[median]
+ end
+
+ 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
+
+ def median
+ render json: { value: data_collector.median.seconds }
+ end
+
+ def average
+ render json: { value: data_collector.average.seconds }
+ end
+
+ def records
+ serialized_records = data_collector.serialized_records do |relation|
+ add_pagination_headers(relation)
+ end
+
+ render json: serialized_records
+ end
+
+ def count
+ render json: { count: data_collector.count }
+ end
+
+ private
+
+ def parent
+ raise NotImplementedError
+ end
+
+ def value_stream_class
+ raise NotImplementedError
+ end
+
+ def add_pagination_headers(relation)
+ Gitlab::Pagination::OffsetHeaderBuilder.new(
+ request_context: self,
+ per_page: relation.limit_value,
+ page: relation.current_page,
+ next_page: relation.next_page,
+ prev_page: relation.prev_page,
+ params: permitted_cycle_analytics_params
+ ).execute(exclude_total_headers: true, data_without_counts: true)
+ end
+
+ def stage
+ @stage ||= ::Analytics::CycleAnalytics::StageFinder.new(parent: parent, stage_id: params[:id]).execute
+ end
+
+ def data_collector
+ @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(
+ stage: stage,
+ params: request_params.to_data_collector_params
+ )
+ end
+
+ def value_stream
+ @value_stream ||= value_stream_class.build_default_value_stream(parent)
+ end
+
+ def list_params
+ { value_stream: value_stream }
+ end
+
+ def list_service
+ Analytics::CycleAnalytics::Stages::ListService.new(parent: parent, 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
+ end
+end
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index b74e343f90b..626093b4588 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -16,9 +16,20 @@ module CycleAnalyticsParams
end
def options(params)
- @options ||= { from: start_date(params), current_user: current_user }.merge(date_range(params))
+ @options ||= {}.tap do |opts|
+ opts[:current_user] = current_user
+ opts[:projects] = params[:project_ids] if params[:project_ids]
+ opts[:group] = params[:group_id] if params[:group_id]
+ opts[:from] = params[:from] || start_date(params)
+ opts[:to] = params[:to] if params[:to]
+ opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
+ opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
+ opts.merge!(date_range(params))
+ end
end
+ private
+
def start_date(params)
case params[:start_date]
when '7'
@@ -41,6 +52,27 @@ module CycleAnalyticsParams
date = field.is_a?(Date) || field.is_a?(Time) ? field : Date.parse(field)
date.to_time.utc
end
+
+ def permitted_cycle_analytics_params
+ params.permit(*::Gitlab::Analytics::CycleAnalytics::RequestParams::STRONG_PARAMS_DEFINITION)
+ end
+
+ def all_cycle_analytics_params
+ permitted_cycle_analytics_params.merge(current_user: current_user)
+ end
+
+ def request_params
+ @request_params ||= ::Gitlab::Analytics::CycleAnalytics::RequestParams.new(all_cycle_analytics_params)
+ end
+
+ def validate_params
+ if request_params.invalid?
+ render(
+ json: { message: 'Invalid parameters', errors: request_params.errors },
+ status: :unprocessable_entity
+ )
+ end
+ end
end
CycleAnalyticsParams.prepend_mod_with('CycleAnalyticsParams')
diff --git a/app/controllers/concerns/dependency_proxy/auth.rb b/app/controllers/concerns/dependency_proxy/auth.rb
deleted file mode 100644
index 1276feedba6..00000000000
--- a/app/controllers/concerns/dependency_proxy/auth.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module DependencyProxy
- module Auth
- extend ActiveSupport::Concern
-
- included do
- # We disable `authenticate_user!` since the `DependencyProxy::Auth` performs auth using JWT token
- skip_before_action :authenticate_user!, raise: false
- prepend_before_action :authenticate_user_from_jwt_token!
- end
-
- def authenticate_user_from_jwt_token!
- return unless dependency_proxy_for_private_groups?
-
- authenticate_with_http_token do |token, _|
- user = user_from_token(token)
- sign_in(user) if user
- end
-
- request_bearer_token! unless current_user
- end
-
- private
-
- def dependency_proxy_for_private_groups?
- Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
- end
-
- def request_bearer_token!
- # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
- response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
- render plain: '', status: :unauthorized
- end
-
- def user_from_token(token)
- token_payload = DependencyProxy::AuthTokenService.decoded_token_payload(token)
- User.find(token_payload['user_id'])
- rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
- nil
- end
- end
-end
diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb
index 2a923d02752..07aca72b22f 100644
--- a/app/controllers/concerns/dependency_proxy/group_access.rb
+++ b/app/controllers/concerns/dependency_proxy/group_access.rb
@@ -12,15 +12,15 @@ module DependencyProxy
private
def verify_dependency_proxy_enabled!
- render_404 unless group.dependency_proxy_feature_available?
+ render_404 unless group&.dependency_proxy_feature_available?
end
def authorize_read_dependency_proxy!
- access_denied! unless can?(current_user, :read_dependency_proxy, group)
+ access_denied! unless can?(auth_user, :read_dependency_proxy, group)
end
def authorize_admin_dependency_proxy!
- access_denied! unless can?(current_user, :admin_dependency_proxy, group)
+ access_denied! unless can?(auth_user, :admin_dependency_proxy, group)
end
end
end
diff --git a/app/controllers/concerns/find_snippet.rb b/app/controllers/concerns/find_snippet.rb
index d51f1a1b3ad..8a4adbb608f 100644
--- a/app/controllers/concerns/find_snippet.rb
+++ b/app/controllers/concerns/find_snippet.rb
@@ -9,7 +9,7 @@ module FindSnippet
# rubocop:disable CodeReuse/ActiveRecord
def snippet
strong_memoize(:snippet) do
- snippet_klass.inc_relations_for_view.find_by(id: snippet_id)
+ snippet_klass.inc_relations_for_view.find_by(snippet_find_params)
end
end
# rubocop:enable CodeReuse/ActiveRecord
@@ -21,4 +21,8 @@ module FindSnippet
def snippet_id
params[:id]
end
+
+ def snippet_find_params
+ { id: snippet_id }
+ end
end
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index f1fa5c845e2..dd066cc1b02 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -5,8 +5,9 @@ module IntegrationsActions
included do
include Integrations::Params
+ include IntegrationsHelper
- before_action :integration, only: [:edit, :update, :test]
+ before_action :integration, only: [:edit, :update, :overrides, :test]
end
def edit
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 2664a7b7151..7ee680db7f9 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -4,6 +4,9 @@ module IssuableActions
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include Gitlab::Cache::Helpers
+ include SpammableActions::AkismetMarkAsSpamAction
+ include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
+ include SpammableActions::CaptchaCheck::JsonFormatActionsSupport
included do
before_action :authorize_destroy_issuable!, only: :destroy
@@ -25,17 +28,42 @@ module IssuableActions
end
def update
- @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- respond_to do |format|
- format.html do
- recaptcha_check_if_spammable { render :edit }
+ updated_issuable = update_service.execute(issuable)
+ # NOTE: We only assign the instance variable on this line, and use the local variable
+ # everywhere else in the method, to avoid having to add multiple `rubocop:disable` comments.
+ @issuable = updated_issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ # NOTE: This check for `is_a?(Spammable)` is necessary because not all
+ # possible `issuable` types implement Spammable. Once they all implement Spammable,
+ # this check can be removed.
+ if updated_issuable.is_a?(Spammable)
+ respond_to do |format|
+ format.html do
+ # NOTE: This redirect is intentionally only performed in the case where the updated
+ # issuable is a spammable, and intentionally is not performed in the non-spammable case.
+ # This preserves the legacy behavior of this action.
+ if updated_issuable.valid?
+ redirect_to spammable_path
+ else
+ with_captcha_check_html_format { render :edit }
+ end
+ end
+
+ format.json do
+ with_captcha_check_json_format { render_entity_json }
+ end
end
-
- format.json do
- recaptcha_check_if_spammable(false) { render_entity_json }
+ else
+ respond_to do |format|
+ format.html do
+ render :edit
+ end
+
+ format.json do
+ render_entity_json
+ end
end
end
-
rescue ActiveRecord::StaleObjectError
render_conflict_response
end
@@ -171,12 +199,6 @@ module IssuableActions
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
- def recaptcha_check_if_spammable(should_redirect = true, &block)
- return yield unless issuable.is_a? Spammable
-
- recaptcha_check_with_fallback(should_redirect, &block)
- end
-
def render_conflict_response
respond_to do |format|
format.html do
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 55e0ed8cd42..97df3c7caea 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -4,6 +4,7 @@
# - a `#container` accessor
# - a `#project` accessor
# - a `#user` accessor
+# - a `#deploy_token` accessor
# - a `#authentication_result` accessor
# - a `#can?(object, action, subject)` method
# - a `#ci?` method
@@ -83,26 +84,18 @@ module LfsRequest
end
def deploy_token_can_download_code?
- deploy_token_present? &&
+ deploy_token.present? &&
deploy_token.project == project &&
deploy_token.active? &&
deploy_token.read_repository?
end
- def deploy_token_present?
- user && user.is_a?(DeployToken)
- end
-
- def deploy_token
- user
- end
-
def lfs_upload_access?
strong_memoize(:lfs_upload_access) do
next false unless has_authentication_ability?(:push_code)
next false if limit_exceeded?
- lfs_deploy_token? || can?(user, :push_code, project)
+ lfs_deploy_token? || can?(user, :push_code, project) || can?(deploy_token, :push_code, project)
end
end
@@ -111,7 +104,7 @@ module LfsRequest
end
def user_can_download_code?
- has_authentication_ability?(:download_code) && can?(user, :download_code, project) && !deploy_token_present?
+ has_authentication_ability?(:download_code) && can?(user, :download_code, project)
end
def build_can_download_code?
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
deleted file mode 100644
index eb1223f22a9..00000000000
--- a/app/controllers/concerns/spammable_actions.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module SpammableActions
- extend ActiveSupport::Concern
- include Spam::Concerns::HasSpamActionResponseFields
-
- included do
- before_action :authorize_submit_spammable!, only: :mark_as_spam
- end
-
- def mark_as_spam
- if Spam::MarkAsSpamService.new(target: spammable).execute
- redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
- else
- redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
- end
- end
-
- private
-
- def recaptcha_check_with_fallback(should_redirect = true, &fallback)
- if should_redirect && spammable.valid?
- redirect_to spammable_path
- elsif spammable.render_recaptcha?
- Gitlab::Recaptcha.load_configurations!
-
- respond_to do |format|
- format.html do
- # NOTE: format.html is still used by issue create, and uses the legacy HAML
- # `_recaptcha_form.html.haml` rendered via the `projects/issues/verify` template.
- render :verify
- end
-
- format.json do
- # format.json is used by all new Vue-based CAPTCHA implementations, which
- # handle all of the CAPTCHA form rendering on the client via the Pajamas-based
- # app/assets/javascripts/captcha/captcha_modal.vue
-
- # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response
- # which requires a CAPTCHA to be solved in order for the request to be resubmitted.
- # See https://stackoverflow.com/q/26547466/25192
- render json: spam_action_response_fields(spammable), status: :conflict
- end
- end
- else
- yield
- end
- end
-
- # TODO: This method is currently only needed for issue create, to convert spam/CAPTCHA values from
- # params, and instead be passed as headers, as the spam services now all expect. It can be removed
- # when issue create is is converted to a client/JS based approach instead of the legacy HAML
- # `_recaptcha_form.html.haml` which is rendered via the `projects/issues/verify` template.
- # In that case, which is based on the legacy reCAPTCHA implementation using the HTML/HAML form,
- # the 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
- # recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form.
- def extract_legacy_spam_params_to_headers
- request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] || params[:captcha_response]
- request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id]
- end
-
- def spammable
- raise NotImplementedError, "#{self.class} does not implement #{__method__}"
- end
-
- def spammable_path
- raise NotImplementedError, "#{self.class} does not implement #{__method__}"
- end
-
- def authorize_submit_spammable!
- access_denied! unless current_user.admin?
- end
-end
diff --git a/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb
new file mode 100644
index 00000000000..234c591ffb7
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module SpammableActions::AkismetMarkAsSpamAction
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+
+ included do
+ before_action :authorize_submit_spammable!, only: :mark_as_spam
+ end
+
+ def mark_as_spam
+ if Spam::AkismetMarkAsSpamService.new(target: spammable).execute
+ redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
+ else
+ redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
+ end
+ end
+
+ private
+
+ def authorize_submit_spammable!
+ access_denied! unless current_user.can_admin_all_resources?
+ end
+
+ def spammable_path
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/attributes.rb b/app/controllers/concerns/spammable_actions/attributes.rb
new file mode 100644
index 00000000000..d7060e47c07
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/attributes.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SpammableActions
+ module Attributes
+ extend ActiveSupport::Concern
+
+ private
+
+ def spammable
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/common.rb b/app/controllers/concerns/spammable_actions/captcha_check/common.rb
new file mode 100644
index 00000000000..7c047e02a1d
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/common.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module SpammableActions::CaptchaCheck
+ module Common
+ extend ActiveSupport::Concern
+
+ private
+
+ def with_captcha_check_common(captcha_render_lambda:, &block)
+ # If the Spammable indicates that CAPTCHA is not necessary (either due to it not being flagged
+ # as spam, or if spam/captcha is disabled for some reason), then we will go ahead and
+ # yield to the block containing the action's original behavior, then return.
+ return yield unless spammable.render_recaptcha?
+
+ # If we got here, we need to render the CAPTCHA instead of yielding to action's original
+ # behavior. We will present a CAPTCHA to be solved by executing the lambda which was passed
+ # as the `captcha_render_lambda:` argument. This lambda contains either the HTML-specific or
+ # JSON-specific behavior to cause the CAPTCHA modal to be rendered.
+ Gitlab::Recaptcha.load_configurations!
+ captcha_render_lambda.call
+ end
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb
new file mode 100644
index 00000000000..f687c0fcf2d
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# This module should *ONLY* be included if needed to support forms submits with HTML MIME type.
+# In other words, forms handled by actions which use a `respond_to` of `format.html`.
+#
+# If the request is handled by actions via `format.json`, for example, for all Javascript based form
+# submissions and Vue components which use Apollo and Axios, then the corresponding module
+# which supports JSON format should be used instead.
+module SpammableActions::CaptchaCheck::HtmlFormatActionsSupport
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+ include SpammableActions::CaptchaCheck::Common
+
+ included do
+ before_action :convert_html_spam_params_to_headers, only: [:create, :update]
+ end
+
+ private
+
+ def with_captcha_check_html_format(&block)
+ captcha_render_lambda = -> { render :captcha_check }
+ with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block)
+ end
+
+ # Convert spam/CAPTCHA values from form field params to headers, because all spam-related services
+ # expect these values to be passed as headers.
+ #
+ # The 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the
+ # recaptcha gem. This is a field which is automatically included by calling the
+ # `#recaptcha_tags` method within a HAML template's form.
+ def convert_html_spam_params_to_headers
+ request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] if params['g-recaptcha-response']
+ request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id] if params[:spam_log_id]
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb
new file mode 100644
index 00000000000..0bfea05abc7
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# This module should be included to support forms submits with a 'js' or 'json' type of MIME type.
+# In other words, forms handled by actions which use a `respond_to` of `format.js` or `format.json`.
+#
+# For example, for all Javascript based form submissions and Vue components which use Apollo and Axios
+#
+# If the request is handled by actions via `format.html`, then the corresponding module which
+# supports HTML format should be used instead.
+module SpammableActions::CaptchaCheck::JsonFormatActionsSupport
+ extend ActiveSupport::Concern
+ include SpammableActions::Attributes
+ include SpammableActions::CaptchaCheck::Common
+ include Spam::Concerns::HasSpamActionResponseFields
+
+ private
+
+ def with_captcha_check_json_format(&block)
+ # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response
+ # which requires a CAPTCHA to be solved in order for the request to be resubmitted.
+ # https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.10
+ captcha_render_lambda = -> { render json: spam_action_response_fields(spammable), status: :conflict }
+ with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block)
+ end
+end
diff --git a/app/controllers/customers_dot/proxy_controller.rb b/app/controllers/customers_dot/proxy_controller.rb
deleted file mode 100644
index 5abf8a487c6..00000000000
--- a/app/controllers/customers_dot/proxy_controller.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module CustomersDot
- class ProxyController < ApplicationController
- skip_before_action :authenticate_user!
- skip_before_action :verify_authenticity_token
-
- feature_category :purchase
-
- BASE_URL = Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL
-
- def graphql
- response = Gitlab::HTTP.post("#{BASE_URL}/graphql",
- body: request.raw_post,
- headers: { 'Content-Type' => 'application/json' }
- )
-
- render json: response.body, status: response.code
- end
- end
-end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 01bb930a51b..d861ef646f8 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -81,7 +81,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(projects)
- projects.includes(:route, :creator, :group, namespace: [:route, :owner]).preload(:project_feature)
+ projects.includes(:route, :creator, :group, :topics, namespace: [:route, :owner]).preload(:project_feature)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 5ef973e9bf3..e0973b0f3b4 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -87,7 +87,7 @@ class Explore::ProjectsController < Explore::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(projects)
- projects.includes(:route, :creator, :group, :project_feature, namespace: [:route, :owner])
+ projects.includes(:route, :creator, :group, :project_feature, :topics, namespace: [:route, :owner])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 69081835c4d..aa0d49902c3 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -13,16 +13,8 @@ class Groups::ApplicationController < ApplicationController
before_action :set_sorting
requires_cross_project_access
- helper_method :can_manage_members?
-
private
- def can_manage_members?(group = @group)
- strong_memoize(:can_manage_members) do
- can?(current_user, :admin_group_member, group)
- end
- end
-
def group
@group ||= find_routable!(Group, params[:group_id] || params[:id], request.path_info)
end
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 04b4d8ea9a7..96a3b38669d 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -7,7 +7,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
- push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
+ push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: :yaml)
push_frontend_feature_flag(:issue_boards_filtered_search, group, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
diff --git a/app/controllers/groups/clusters/applications_controller.rb b/app/controllers/groups/clusters/applications_controller.rb
deleted file mode 100644
index ce6fda4143c..00000000000
--- a/app/controllers/groups/clusters/applications_controller.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-class Groups::Clusters::ApplicationsController < Clusters::ApplicationsController
- include ControllerWithCrossProjectAccessCheck
-
- prepend_before_action :group
- requires_cross_project_access
-
- private
-
- def clusterable
- @clusterable ||= ClusterablePresenter.fabricate(group, current_user: current_user)
- end
-
- def group
- @group ||= find_routable!(Group, params[:group_id] || params[:id], request.path_info)
- end
-end
diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb
index b896b240daf..b037aa52939 100644
--- a/app/controllers/groups/dependency_proxies_controller.rb
+++ b/app/controllers/groups/dependency_proxies_controller.rb
@@ -2,7 +2,7 @@
module Groups
class DependencyProxiesController < Groups::ApplicationController
- include DependencyProxy::GroupAccess
+ include ::DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update
before_action :dependency_proxy
diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb
new file mode 100644
index 00000000000..fd9db41f748
--- /dev/null
+++ b/app/controllers/groups/dependency_proxy/application_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Groups
+ module DependencyProxy
+ class ApplicationController < ::ApplicationController
+ EMPTY_AUTH_RESULT = Gitlab::Auth::Result.new(nil, nil, nil, nil).freeze
+
+ delegate :actor, to: :@authentication_result, allow_nil: true
+
+ # This allows auth_user to be set in the base ApplicationController
+ alias_method :authenticated_user, :actor
+
+ # We disable `authenticate_user!` since the `DependencyProxy::ApplicationController` performs auth using JWT token
+ skip_before_action :authenticate_user!, raise: false
+
+ prepend_before_action :authenticate_user_from_jwt_token!
+
+ def authenticate_user_from_jwt_token!
+ return unless dependency_proxy_for_private_groups?
+
+ authenticate_with_http_token do |token, _|
+ @authentication_result = EMPTY_AUTH_RESULT
+
+ found_user = user_from_token(token)
+ sign_in(found_user) if found_user.is_a?(User)
+ end
+
+ request_bearer_token! unless authenticated_user
+ end
+
+ private
+
+ def dependency_proxy_for_private_groups?
+ Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
+ end
+
+ def request_bearer_token!
+ # unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
+ response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
+ render plain: '', status: :unauthorized
+ end
+
+ def user_from_token(token)
+ token_payload = ::DependencyProxy::AuthTokenService.decoded_token_payload(token)
+
+ if token_payload['user_id']
+ token_user = User.find(token_payload['user_id'])
+ return unless token_user
+
+ @authentication_result = Gitlab::Auth::Result.new(token_user, nil, :user, [])
+ return token_user
+ elsif token_payload['deploy_token']
+ deploy_token = DeployToken.active.find_by_token(token_payload['deploy_token'])
+ return unless deploy_token
+
+ @authentication_result = Gitlab::Auth::Result.new(deploy_token, nil, :deploy_token, [])
+ return deploy_token
+ end
+
+ nil
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
+ nil
+ end
+ end
+ end
+end
diff --git a/app/controllers/groups/dependency_proxy_auth_controller.rb b/app/controllers/groups/dependency_proxy_auth_controller.rb
index e3e9bd88e24..60b2371fa9a 100644
--- a/app/controllers/groups/dependency_proxy_auth_controller.rb
+++ b/app/controllers/groups/dependency_proxy_auth_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-class Groups::DependencyProxyAuthController < ApplicationController
- include DependencyProxy::Auth
-
+class Groups::DependencyProxyAuthController < ::Groups::DependencyProxy::ApplicationController
feature_category :dependency_proxy
def authenticate
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index e2c104f88a4..f7dc552bd3e 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
-class Groups::DependencyProxyForContainersController < Groups::ApplicationController
- include DependencyProxy::Auth
+class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy::ApplicationController
+ include Gitlab::Utils::StrongMemoize
include DependencyProxy::GroupAccess
include SendFileUpload
+ include ::PackagesHelper # for event tracking
+ before_action :ensure_group
before_action :ensure_token_granted!
before_action :ensure_feature_enabled!
@@ -22,6 +24,8 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
response.headers['Etag'] = "\"#{result[:manifest].digest}\""
content_type = result[:manifest].content_type
+ event_name = tracking_event_name(object_type: :manifest, from_cache: result[:from_cache])
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
send_upload(
result[:manifest].file,
proxy: true,
@@ -38,6 +42,8 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
.new(group, image, token, params[:sha]).execute
if result[:status] == :success
+ event_name = tracking_event_name(object_type: :blob, from_cache: result[:from_cache])
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
send_upload(result[:blob].file)
else
head result[:http_status]
@@ -46,6 +52,12 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
private
+ def group
+ strong_memoize(:group) do
+ Group.find_by_full_path(params[:group_id], follow_redirects: request.get?)
+ end
+ end
+
def image
params[:image]
end
@@ -54,11 +66,22 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
params[:tag]
end
+ def tracking_event_name(object_type:, from_cache:)
+ event_name = "pull_#{object_type}"
+ event_name = "#{event_name}_from_cache" if from_cache
+
+ event_name
+ end
+
def dependency_proxy
@dependency_proxy ||=
group.dependency_proxy_setting || group.create_dependency_proxy_setting
end
+ def ensure_group
+ render_404 unless group
+ end
+
def ensure_feature_enabled!
render_404 unless dependency_proxy.enabled
end
diff --git a/app/controllers/groups/email_campaigns_controller.rb b/app/controllers/groups/email_campaigns_controller.rb
index d4c7b31c4b8..70c8a23d918 100644
--- a/app/controllers/groups/email_campaigns_controller.rb
+++ b/app/controllers/groups/email_campaigns_controller.rb
@@ -38,10 +38,12 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
create_track_url
when :verify
project_pipelines_url(group.projects.first)
- when :trial
+ when :trial, :trial_short
'https://about.gitlab.com/free-trial/'
- when :team
+ when :team, :team_short
group_group_members_url(group)
+ when :admin_verify
+ project_settings_ci_cd_path(group.projects.first, ci_runner_templates: true, anchor: 'js-runners-settings')
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index d5e7653dea2..9b8d5cfe476 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -29,7 +29,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
- if can_manage_members?
+ if can?(current_user, :admin_group_member, @group)
@skip_groups = @group.related_group_ids
@invited_members = @members.invite
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 1cff658dd52..dbbfdd76fe8 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -1,14 +1,21 @@
# frozen_string_literal: true
class Groups::RunnersController < Groups::ApplicationController
- # Proper policies should be implemented per
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/45894
+ # TODO Proper policies, such as `read_group_runners, should be implemented per
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/334802
before_action :authorize_admin_group!
-
+ before_action :runner_list_group_view_vue_ui_enabled, only: [:index]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
feature_category :runner
+ def index
+ end
+
+ def runner_list_group_view_vue_ui_enabled
+ return render_404 unless Feature.enabled?(:runner_list_group_view_vue_ui, group, default_enabled: :yaml)
+ end
+
def show
end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index 88c709e3f53..0f40c9bfd2c 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -60,6 +60,7 @@ module Groups
def define_variables
define_ci_variables
+ define_view_variables
end
def define_ci_variables
@@ -69,6 +70,10 @@ module Groups
.map { |variable| variable.present(current_user: current_user) }
end
+ def define_view_variables
+ @content_class = 'limit-container-width' unless fluid_layout
+ end
+
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index 8e3b2cb5d1b..a7a1de03224 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -26,10 +26,6 @@ module Groups
def find_or_initialize_non_project_specific_integration(name)
Integration.find_or_initialize_non_project_specific_integration(name, group_id: group.id)
end
-
- def scoped_edit_integration_path(integration)
- edit_group_settings_integration_path(group, integration)
- end
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 66816d4c587..2796760fbe1 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -206,8 +206,6 @@ class GroupsController < Groups::ApplicationController
protected
def render_show_html
- record_experiment_user(:invite_members_empty_group_version_a) if ::Gitlab.com?
-
render 'groups/show', locals: { trial: params[:trial] }
end
diff --git a/app/controllers/import/available_namespaces_controller.rb b/app/controllers/import/available_namespaces_controller.rb
index c6211b33d28..0c2af13d3f3 100644
--- a/app/controllers/import/available_namespaces_controller.rb
+++ b/app/controllers/import/available_namespaces_controller.rb
@@ -4,6 +4,6 @@ class Import::AvailableNamespacesController < ApplicationController
feature_category :importers
def index
- render json: NamespaceSerializer.new.represent(current_user.manageable_groups_with_routes)
+ render json: NamespaceSerializer.new.represent(current_user.manageable_groups_with_routes(include_groups_with_developer_maintainer_access: true))
end
end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 1121ecfb65c..53856e4575b 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -11,8 +11,7 @@ class Import::BaseController < ApplicationController
format.json do
render json: { imported_projects: serialized_imported_projects,
provider_repos: serialized_provider_repos,
- incompatible_repos: serialized_incompatible_repos,
- namespaces: serialized_namespaces }
+ incompatible_repos: serialized_incompatible_repos }
end
format.html
end
@@ -74,14 +73,6 @@ class Import::BaseController < ApplicationController
@already_added_projects ||= filtered(find_already_added_projects(provider_name))
end
- def serialized_namespaces
- NamespaceSerializer.new.represent(namespaces)
- end
-
- def namespaces
- current_user.manageable_groups_with_routes
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def find_already_added_projects(import_type)
current_user.created_projects.where(import_type: import_type).with_import_state
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 3c81b698546..7f5750d2011 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -75,7 +75,10 @@ class InvitesController < ApplicationController
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])
+ return unless member && initial_invite_email?
+
+ experiment(:invite_email_preview_text, actor: member).track(:join_clicked) if params[:experiment_name] == 'invite_email_preview_text'
+ Gitlab::Tracking.event(self.class.name, 'join_clicked', label: 'invite_email', property: member.id.to_s)
end
def authenticate_user!
@@ -95,7 +98,12 @@ class InvitesController < ApplicationController
def set_session_invite_params
session[:invite_email] = member.invite_email
- session[:originating_member_id] = member.id if Members::InviteEmailExperiment.initial_invite_email?(params[:invite_type])
+ session[:originating_member_id] = member.id if initial_invite_email?
+ session[:invite_email_experiment_name] = params[:experiment_name] if initial_invite_email? && params[:experiment_name]
+ end
+
+ def initial_invite_email?
+ params[:invite_type] == Emails::Members::INITIAL_INVITE
end
def sign_in_redirect_params
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index 0de42ad2452..a0f387631dd 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -44,27 +44,14 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
def modules
modules = {
- jiraDevelopmentTool: {
- key: 'gitlab-development-tool',
- application: {
- value: 'GitLab'
- },
- name: {
- value: 'GitLab'
- },
- url: HOME_URL,
- logoUrl: logo_url,
- capabilities: %w(branch commit pull_request)
- },
postInstallPage: {
key: 'gitlab-configuration',
- name: {
- value: 'GitLab Configuration'
- },
+ name: { value: 'GitLab Configuration' },
url: relative_to_base_path(jira_connect_subscriptions_path)
}
}
+ modules.merge!(development_tool_module)
modules.merge!(build_information_module)
modules.merge!(deployment_information_module)
modules.merge!(feature_flag_module)
@@ -76,6 +63,25 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
view_context.image_url('gitlab_logo.png')
end
+ # See https://developer.atlassian.com/cloud/jira/software/modules/development-tool/
+ def development_tool_module
+ {
+ jiraDevelopmentTool: {
+ actions: {
+ createBranch: {
+ templateUrl: new_jira_connect_branch_url + '?issue_key={issue.key}&issue_summary={issue.summary}'
+ }
+ },
+ key: 'gitlab-development-tool',
+ application: { value: 'GitLab' },
+ name: { value: 'GitLab' },
+ url: HOME_URL,
+ logoUrl: logo_url,
+ capabilities: %w(branch commit pull_request)
+ }
+ }
+ end
+
# See: https://developer.atlassian.com/cloud/jira/software/modules/deployment/
def deployment_information_module
{
@@ -92,9 +98,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
{
jiraFeatureFlagInfoProvider: common_module_properties.merge(
actions: {}, # TODO: create, link and list feature flags https://gitlab.com/gitlab-org/gitlab/-/issues/297386
- name: {
- value: 'GitLab Feature Flags'
- },
+ name: { value: 'GitLab Feature Flags' },
key: 'gitlab-feature-flags'
)
}
diff --git a/app/controllers/jira_connect/branches_controller.rb b/app/controllers/jira_connect/branches_controller.rb
new file mode 100644
index 00000000000..12ea6560e3a
--- /dev/null
+++ b/app/controllers/jira_connect/branches_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# NOTE: This controller does not inherit from JiraConnect::ApplicationController
+# because we don't receive a JWT for this action, so we rely on standard GitLab authentication.
+class JiraConnect::BranchesController < ApplicationController
+ feature_category :integrations
+
+ def new
+ @new_branch_data = new_branch_data
+ end
+
+ private
+
+ def initial_branch_name
+ return unless params[:issue_key].present?
+
+ Issue.to_branch_name(
+ params[:issue_key],
+ params[:issue_summary]
+ )
+ end
+
+ def new_branch_data
+ {
+ initial_branch_name: initial_branch_name,
+ success_state_svg_path: ActionController::Base.helpers.image_path('illustrations/merge_requests.svg')
+ }
+ end
+end
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index 3ff12f29f10..a9c4dbf2b17 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -22,6 +22,13 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
def index
@subscriptions = current_jira_installation.subscriptions.preload_namespace_route
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: JiraConnect::AppDataSerializer.new(@subscriptions, !!current_user).as_json
+ end
+ end
end
def create
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 85ee2204324..010b85e81bf 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -19,7 +19,7 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]]
return head :not_found unless service
- result = service.new(@authentication_result.project, @authentication_result.actor, auth_params)
+ result = service.new(@authentication_result.project, auth_user, auth_params)
.execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status]
@@ -67,7 +67,7 @@ class JwtController < ApplicationController
end
def additional_params
- { scopes: scopes_param }.compact
+ { scopes: scopes_param, deploy_token: @authentication_result.deploy_token }.compact
end
# We have to parse scope here, because Docker Client does not send an array of scopes,
@@ -83,8 +83,7 @@ class JwtController < ApplicationController
def auth_user
strong_memoize(:auth_user) do
- actor = @authentication_result&.actor
- actor.is_a?(User) ? actor : nil
+ @authentication_result.auth_user
end
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e2f8baa8226..effd3514c1b 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -57,6 +57,8 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
@codes = user.generate_otp_backup_codes!
end
+ helpers.dismiss_account_recovery_regular_check
+
render 'create'
else
@error = _('Invalid pin code')
@@ -105,6 +107,8 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def codes
Users::UpdateService.new(current_user, user: current_user).execute! do |user|
@codes = user.generate_otp_backup_codes!
+
+ helpers.dismiss_account_recovery_regular_check
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 505608779ec..6cc602fec88 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -128,6 +128,7 @@ class ProfilesController < Profiles::ApplicationController
:timezone,
:job_title,
:pronouns,
+ :pronunciation,
status: [:emoji, :message, :availability]
)
end
diff --git a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb
index 7b4f6739a9b..2f9d70fede1 100644
--- a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb
+++ b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
+ include ::Analytics::CycleAnalytics::StageActions
+ extend ::Gitlab::Utils::Override
+
respond_to :json
feature_category :planning_analytics
@@ -8,37 +11,19 @@ class Projects::Analytics::CycleAnalytics::StagesController < Projects::Applicat
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
+ override :parent
+ def parent
+ @project
end
- def value_stream
- Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
+ override :value_stream_class
+ def value_stream_class
+ Analytics::CycleAnalytics::ProjectValueStream
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)
+ def only_default_value_stream_is_allowed!
+ render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 08066acb45c..acf6b6116b8 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -44,6 +44,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
+ push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
def new
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
deleted file mode 100644
index 6c5778124e8..00000000000
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
- prepend_before_action :project
-
- private
-
- def clusterable
- @clusterable ||= ClusterablePresenter.fabricate(project, current_user: current_user)
- end
-
- def project
- @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), request.path_info)
- end
-end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index edf45e7063a..99f62c18593 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -20,10 +20,6 @@ class Projects::CompareController < Projects::ApplicationController
# Validation
before_action :validate_refs!
- before_action do
- push_frontend_feature_flag(:compare_repo_dropdown, source_project, default_enabled: :yaml)
- end
-
feature_category :source_code_management
# Diffs may be pretty chunky, the less is better in this endpoint.
@@ -91,7 +87,6 @@ class Projects::CompareController < Projects::ApplicationController
def target_project
strong_memoize(:target_project) do
next source_project unless params.key?(:from_project_id)
- next source_project unless Feature.enabled?(:compare_repo_dropdown, source_project, default_enabled: :yaml)
next source_project if params[:from_project_id].to_i == source_project.id
target_project = target_projects(source_project).find_by_id(params[:from_project_id])
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 8519841ee16..cac0aa9d513 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -87,17 +87,17 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment = project.environments.create(environment_params)
if @environment.persisted?
- redirect_to project_environment_path(project, @environment)
+ render json: { environment: @environment, path: project_environment_path(project, @environment) }
else
- render :new
+ render json: { message: @environment.errors.full_messages }, status: :bad_request
end
end
def update
if @environment.update(environment_params)
- redirect_to project_environment_path(project, @environment)
+ render json: { environment: @environment, path: project_environment_path(project, @environment) }
else
- render :edit
+ render json: { message: @environment.errors.full_messages }, status: :bad_request
end
end
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
index b4b03e219ab..8700d3c2198 100644
--- a/app/controllers/projects/error_tracking_controller.rb
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -27,7 +27,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
end
def update
- service = ErrorTracking::IssueUpdateService.new(project, current_user, issue_update_params)
+ service = ::ErrorTracking::IssueUpdateService.new(project, current_user, issue_update_params)
result = service.execute
return if render_errors(result)
@@ -40,7 +40,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
private
def render_index_json
- service = ErrorTracking::ListIssuesService.new(
+ service = ::ErrorTracking::ListIssuesService.new(
project,
current_user,
list_issues_params
@@ -57,7 +57,7 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
end
def render_issue_detail_json
- service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
+ service = ::ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
result = service.execute
return if render_errors(result)
@@ -91,13 +91,13 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
end
def serialize_errors(errors)
- ErrorTracking::ErrorSerializer
+ ::ErrorTracking::ErrorSerializer
.new(project: project, user: current_user)
.represent(errors)
end
def serialize_detailed_error(error)
- ErrorTracking::DetailedErrorSerializer
+ ::ErrorTracking::DetailedErrorSerializer
.new(project: project, user: current_user)
.represent(error)
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 0f00fda4687..7135c0d959e 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -99,7 +99,7 @@ class Projects::ForksController < Projects::ApplicationController
current_user: current_user
).execute
- forks.includes(:route, :creator, :group, namespace: [:route, :owner])
+ forks.includes(:route, :creator, :group, :topics, namespace: [:route, :owner])
end
def fork_service
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 5d38e431c8a..bdfaaf2b143 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -7,7 +7,6 @@ class Projects::IssuesController < Projects::ApplicationController
include ToggleAwardEmoji
include IssuableCollections
include IssuesCalendar
- include SpammableActions
include RecordUserLastActivity
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
@@ -58,7 +57,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
- experiment_instance.exclude! unless helpers.can_import_members?
+ experiment_instance.exclude! unless helpers.can_admin_project_member?(@project)
experiment_instance.use {}
experiment_instance.try(:invite_member_link) {}
@@ -129,7 +128,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- extract_legacy_spam_params_to_headers
create_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
@@ -149,10 +147,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :new }
- end
+ if @issue.valid?
+ redirect_to project_issue_path(@project, @issue)
+ else
+ # NOTE: this CAPTCHA support method is indirectly included via IssuableActions
+ with_captcha_check_html_format { render :new }
end
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 49687a50ff6..778623a05c6 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build_trace!, only: [:trace, :raw]
before_action :authorize_read_build!
before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :erase]
+ except: [:index, :show, :status, :raw, :trace, :erase, :cancel, :unschedule]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
@@ -93,22 +93,28 @@ class Projects::JobsController < Projects::ApplicationController
end
def cancel
- return respond_422 unless @build.cancelable?
+ service_response = Ci::BuildCancelService.new(@build, current_user).execute
- @build.cancel
-
- if continue_params[:to]
- redirect_to continue_params[:to]
+ if service_response.success?
+ destination = continue_params[:to].presence || builds_project_pipeline_path(@project, @build.pipeline.id)
+ redirect_to destination
+ elsif service_response.http_status == :forbidden
+ access_denied!
else
- redirect_to builds_project_pipeline_path(@project, @build.pipeline.id)
+ head service_response.http_status
end
end
def unschedule
- return respond_422 unless @build.scheduled?
+ service_response = Ci::BuildUnscheduleService.new(@build, current_user).execute
- @build.unschedule!
- redirect_to build_path(@build)
+ if service_response.success?
+ redirect_to build_path(@build)
+ elsif service_response.http_status == :forbidden
+ access_denied!
+ else
+ head service_response.http_status
+ end
end
def status
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 9f1e2d8236a..ecc5ad1f84e 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -10,10 +10,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
- before_action do
- push_frontend_feature_flag(:mr_collapsed_approval_rules, @project)
- end
-
def new
define_new_vars
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 88423bec915..8ccc658dfe7 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -27,10 +27,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
diff_options_hash[:paths] = params[:paths] if params[:paths]
diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash)
- positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user)
+ unfoldable_positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user).unfoldable
environment = @merge_request.environments_for(current_user, latest: true).last
- diffs.unfold_diff_files(positions.unfoldable)
+ diffs.unfold_diff_files(unfoldable_positions)
diffs.write_cache
options = {
@@ -38,14 +38,29 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
merge_request: @merge_request,
diff_view: diff_view,
merge_ref_head_diff: render_merge_ref_head_diff?,
- pagination_data: diffs.pagination_data
+ pagination_data: diffs.pagination_data,
+ allow_tree_conflicts: display_merge_conflicts_in_diff?
}
if diff_options_hash[:paths].blank? && Feature.enabled?(:diffs_batch_render_cached, project, default_enabled: :yaml)
+ # NOTE: Any variables that would affect the resulting json needs to be added to the cache_context to avoid stale cache issues.
+ cache_context = [
+ current_user&.cache_key,
+ environment&.cache_key,
+ unfoldable_positions.map(&:to_h),
+ diff_view,
+ params[:w],
+ params[:expanded],
+ params[:page],
+ params[:per_page],
+ options[:merge_ref_head_diff],
+ options[:allow_tree_conflicts]
+ ]
+
render_cached(
diffs,
with: PaginatedDiffSerializer.new(current_user: current_user),
- cache_context: -> (_) { [diff_view, params[:w], params[:expanded], params[:per_page], params[:page]] },
+ cache_context: -> (_) { [Digest::SHA256.hexdigest(cache_context.to_s)] },
**options
)
else
@@ -56,8 +71,14 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def diffs_metadata
diffs = @compare.diffs(diff_options)
+ options = additional_attributes.merge(
+ only_context_commits: show_only_context_commits?,
+ merge_ref_head_diff: render_merge_ref_head_diff?,
+ allow_tree_conflicts: display_merge_conflicts_in_diff?
+ )
+
render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user)
- .represent(diffs, additional_attributes.merge(only_context_commits: show_only_context_commits?))
+ .represent(diffs, options)
end
private
@@ -82,7 +103,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
options = additional_attributes.merge(
diff_view: "inline",
- merge_ref_head_diff: render_merge_ref_head_diff?
+ merge_ref_head_diff: render_merge_ref_head_diff?,
+ allow_tree_conflicts: display_merge_conflicts_in_diff?
)
if @merge_request.project.context_commits_enabled?
@@ -213,4 +235,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
.track_mr_diffs_single_file_action(merge_request: @merge_request, user: current_user)
end
+
+ def display_merge_conflicts_in_diff?
+ Feature.enabled?(:display_merge_conflicts_in_diff, @merge_request.project)
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index cfa64bbc16d..8b3f2df69df 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -11,6 +11,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include RecordUserLastActivity
include SourcegraphDecorator
include DiffHelper
+ include Gitlab::Cache::Helpers
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
before_action :apply_diff_view_cookie!, only: [:show]
@@ -43,9 +44,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
+ push_frontend_feature_flag(:diff_searching_usage_data, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
- experiment_instance.exclude! unless helpers.can_import_members?
+ experiment_instance.exclude! unless helpers.can_admin_project_member?(@project)
experiment_instance.use {}
experiment_instance.try(:invite_member_link) {}
@@ -55,7 +57,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
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
@@ -92,6 +93,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
+ # rubocop:disable Metrics/AbcSize
def show
close_merge_request_if_no_source_project
@@ -128,7 +130,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
- render json: serializer.represent(@merge_request, serializer: params[:serializer])
+ if params[:serializer] == 'sidebar_extras' && Feature.enabled?(:merge_request_show_render_cached, @project, default_enabled: :yaml)
+ cache_context = [
+ params[:serializer],
+ current_user&.cache_key,
+ @merge_request.assignees.map(&:cache_key),
+ @merge_request.reviewers.map(&:cache_key)
+ ]
+
+ render_cached(@merge_request,
+ with: serializer,
+ cache_context: -> (_) { [Digest::SHA256.hexdigest(cache_context.to_s)] },
+ serializer: params[:serializer])
+ else
+ render json: serializer.represent(@merge_request, serializer: params[:serializer])
+ end
end
format.patch do
@@ -144,6 +160,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
end
+ # rubocop:enable Metrics/AbcSize
def commits
# Get context commits from repository
diff --git a/app/controllers/projects/packages/infrastructure_registry_controller.rb b/app/controllers/projects/packages/infrastructure_registry_controller.rb
index ee04cbb0062..4506a83634a 100644
--- a/app/controllers/projects/packages/infrastructure_registry_controller.rb
+++ b/app/controllers/projects/packages/infrastructure_registry_controller.rb
@@ -3,19 +3,14 @@
module Projects
module Packages
class InfrastructureRegistryController < Projects::ApplicationController
- before_action :verify_feature_enabled!
+ include PackagesAccess
+
feature_category :infrastructure_as_code
def show
@package = project.packages.find(params[:id])
@package_files = @package.package_files.recent
end
-
- private
-
- def verify_feature_enabled!
- render_404 unless Feature.enabled?(:infrastructure_registry_page, default_enabled: :yaml)
- end
end
end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index ba7c86434e0..a411264b350 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -12,12 +12,11 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_read_ci_cd_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+ before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
+
before_action do
- push_frontend_feature_flag(:pipeline_graph_layers_view, project, type: :development, 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(:pipeline_source_filter, project, type: :development, default_enabled: :yaml)
end
- 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? }
@@ -32,10 +31,11 @@ class Projects::PipelinesController < Projects::ApplicationController
feature_category :continuous_integration, [
:charts, :show, :config_variables, :stage, :cancel, :retry,
- :builds, :dag, :failures, :status, :downloadable_artifacts,
+ :builds, :dag, :failures, :status,
:index, :create, :new, :destroy
]
feature_category :code_testing, [:test_report]
+ feature_category :build_artifacts, [:downloadable_artifacts]
def index
@pipelines = Ci::PipelinesFinder
@@ -68,20 +68,22 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
- @pipeline = Ci::CreatePipelineService
+ service_response = Ci::CreatePipelineService
.new(project, current_user, create_params)
.execute(:web, ignore_skip_ci: true, save_on_errors: false)
+ @pipeline = service_response.payload
+
respond_to do |format|
format.html do
- if @pipeline.created_successfully?
+ if service_response.success?
redirect_to project_pipeline_path(project, @pipeline)
else
render 'new', status: :bad_request
end
end
format.json do
- if @pipeline.created_successfully?
+ if service_response.success?
render json: PipelineSerializer
.new(project: project, current_user: current_user)
.represent(@pipeline),
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 370cd2b02a1..d0987492d2d 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
.new(@project, current_user, params: filter_params)
.execute(include_relations: requested_relations)
- if helpers.can_manage_project_members?(@project)
+ if can?(current_user, :admin_project_member, @project)
@invited_members = present_members(project_members.invite)
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 3fff93abe5c..e86d2490282 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -19,7 +19,7 @@ class Projects::RawController < Projects::ApplicationController
feature_category :source_code_management
def show
- @blob = @repository.blob_at(@ref, @path)
+ @blob = @repository.blob_at(@ref, @path, limit: Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE)
send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: Guest.can?(:download_code, @project))
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index fa6adc9431d..5da81045e02 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -5,7 +5,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
layout 'project_settings'
- feature_category :continuous_integration
+ feature_category :runner
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb
index 3a473bb67e0..19de157357a 100644
--- a/app/controllers/projects/security/configuration_controller.rb
+++ b/app/controllers/projects/security/configuration_controller.rb
@@ -7,10 +7,6 @@ module Projects
feature_category :static_application_security_testing
- before_action only: [:show] do
- push_frontend_feature_flag(:security_configuration_redesign, project, default_enabled: :yaml)
- end
-
def show
render_403 unless can?(current_user, :read_security_configuration, project)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index ef6d96e8737..0dcaab7160b 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -8,6 +8,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :ensure_service_enabled
before_action :integration
+ before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update]
before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_integration, only: [:update]
@@ -19,14 +20,22 @@ class Projects::ServicesController < Projects::ApplicationController
feature_category :integrations
def edit
- @default_integration = Integration.default_integration(service.type, project)
end
def update
- @integration.attributes = integration_params[:integration]
- @integration.inherit_from_id = nil if integration_params[:integration][:inherit_from_id].blank?
+ attributes = integration_params[:integration]
- saved = @integration.save(context: :manual_change)
+ if use_inherited_settings?(attributes)
+ @integration.inherit_from_id = default_integration.id
+
+ if saved = @integration.save(context: :manual_change)
+ BulkUpdateIntegrationService.new(default_integration, [@integration]).execute
+ end
+ else
+ attributes[:inherit_from_id] = nil
+ @integration.attributes = attributes
+ saved = @integration.save(context: :manual_change)
+ end
respond_to do |format|
format.html do
@@ -88,6 +97,10 @@ class Projects::ServicesController < Projects::ApplicationController
end
alias_method :service, :integration
+ def default_integration
+ @default_integration ||= Integration.default_integration(integration.type, project)
+ end
+
def web_hook_logs
return unless integration.service_hook.present?
@@ -115,4 +128,8 @@ class Projects::ServicesController < Projects::ApplicationController
message = s_('PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page have been deprecated.') % { operations_link_start: operations_link_start, operations_link_end: "</a>" }
flash.now[:alert] = message.html_safe
end
+
+ def use_inherited_settings?(attributes)
+ default_integration && attributes[:inherit_from_id] == default_integration.id.to_s
+ end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index de2ab16b5b1..97f9c5814e2 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
class Projects::SnippetsController < Projects::Snippets::ApplicationController
+ extend ::Gitlab::Utils::Override
include SnippetsActions
include ToggleAwardEmoji
- include SpammableActions
+ include SpammableActions::AkismetMarkAsSpamAction
before_action :check_snippets_available!
@@ -45,4 +46,9 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
def spammable_path
project_snippet_path(@project, @snippet)
end
+
+ override :snippet_find_params
+ def snippet_find_params
+ super.merge(project_id: project.id)
+ end
end
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index df945a99c73..4bad6dc1b3d 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -5,7 +5,7 @@ class Projects::TemplatesController < Projects::ApplicationController
before_action :authorize_can_read_issuable!
before_action :get_template_class
- feature_category :templates
+ feature_category :source_code_management
def index
templates = @template_type.template_subsets(project)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 0dbf7d40f87..bdb645e1934 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -31,10 +31,6 @@ class ProjectsController < Projects::ApplicationController
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
- before_action only: [:edit] do
- push_frontend_feature_flag(:allow_editing_commit_messages, @project)
- end
-
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
@@ -74,11 +70,6 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved?
- experiment(:new_project_readme, actor: current_user).track(
- :created,
- property: active_new_project_tab,
- value: project_params[:initialize_with_readme].to_i
- )
redirect_to(
project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
@@ -397,6 +388,7 @@ class ProjectsController < Projects::ApplicationController
analytics_access_level
operations_access_level
security_and_compliance_access_level
+ container_registry_access_level
]
end
@@ -404,7 +396,6 @@ class ProjectsController < Projects::ApplicationController
%i[
show_default_award_emojis
squash_option
- allow_editing_commit_messages
mr_default_target_self
]
end
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 303ee431a4d..ced21b8f291 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -16,7 +16,7 @@ module Registrations
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
- return redirect_to new_users_sign_up_group_path if show_signup_onboarding?
+ return redirect_to new_users_sign_up_group_path(trial_params) if show_signup_onboarding?
members = current_user.members
@@ -41,7 +41,7 @@ module Registrations
end
def update_params
- params.require(:user).permit(:role, :other_role, :setup_for_company, :email_opted_in)
+ params.require(:user).permit(:role, :other_role, :setup_for_company)
end
def requires_confirmation?(user)
@@ -67,6 +67,10 @@ module Registrations
def show_signup_onboarding?
false
end
+
+ def trial_params
+ nil
+ end
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 7b1060eba8f..cc985e84542 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -199,7 +199,9 @@ class RegistrationsController < Devise::RegistrationsController
return unless member
- experiment('members/invite_email', actor: member).track(:accepted)
+ experiment_name = session.delete(:invite_email_experiment_name)
+ experiment(:invite_email_preview_text, actor: member).track(:accepted) if experiment_name == 'invite_email_preview_text'
+ Gitlab::Tracking.event(self.class.name, 'accepted', label: 'invite_email', property: member.id.to_s)
end
def context_user
diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb
index 76d9983d341..b3adda8c633 100644
--- a/app/controllers/repositories/git_http_client_controller.rb
+++ b/app/controllers/repositories/git_http_client_controller.rb
@@ -33,7 +33,7 @@ module Repositories
end
def authenticate_user
- @authentication_result = Gitlab::Auth::Result.new
+ @authentication_result = Gitlab::Auth::Result::EMPTY
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index 4f2e02c78c3..a7719516cb6 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -10,6 +10,10 @@ module Repositories
skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
+ # added here as a part of the refactor, will be removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328692
+ delegate :deploy_token, :user, to: :authentication_result, allow_nil: true
+
def batch
unless objects.present?
render_lfs_not_found
@@ -141,7 +145,7 @@ module Repositories
end
def lfs_auth_header
- return unless user.is_a?(User)
+ return unless user
Gitlab::LfsToken.new(user).basic_encoding
end
diff --git a/app/controllers/repositories/lfs_locks_api_controller.rb b/app/controllers/repositories/lfs_locks_api_controller.rb
index 19fc09ad4de..1d091a5bfcd 100644
--- a/app/controllers/repositories/lfs_locks_api_controller.rb
+++ b/app/controllers/repositories/lfs_locks_api_controller.rb
@@ -4,6 +4,10 @@ module Repositories
class LfsLocksApiController < Repositories::GitHttpClientController
include LfsRequest
+ # added here as a part of the refactor, will be removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328692
+ delegate :deploy_token, :user, to: :authentication_result, allow_nil: true
+
def create
@result = Lfs::LockFileService.new(project, user, lfs_params).execute
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index 36912948b77..6ec63a0f939 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -8,6 +8,10 @@ module Repositories
skip_before_action :verify_workhorse_api!, only: :download
+ # added here as a part of the refactor, will be removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/328692
+ delegate :deploy_token, :user, to: :authentication_result, allow_nil: true
+
def download
lfs_object = LfsObject.find_by_oid(oid)
unless lfs_object && lfs_object.file.exists?
diff --git a/app/controllers/runner_setup_controller.rb b/app/controllers/runner_setup_controller.rb
index e0e9c5b7c23..89b635d5a6f 100644
--- a/app/controllers/runner_setup_controller.rb
+++ b/app/controllers/runner_setup_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class RunnerSetupController < ApplicationController
- feature_category :continuous_integration
+ feature_category :runner
def platforms
render json: Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 4160b528301..dbddb35d358 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -5,6 +5,8 @@ class SearchController < ApplicationController
include SearchHelper
include RedisTracking
+ RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show].freeze
+
track_redis_hll_event :show, name: 'i_search_total'
around_action :allow_gitaly_ref_name_caching
@@ -37,6 +39,7 @@ class SearchController < ApplicationController
@search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
@scope = @search_service.scope
+ @without_count = @search_service.without_count?
@show_snippets = @search_service.show_snippets?
@search_results = @search_service.search_results
@search_objects = @search_service.search_objects
@@ -154,12 +157,21 @@ class SearchController < ApplicationController
end
def render_timeout(exception)
- raise exception unless action_name.to_sym == :show
+ raise exception unless action_name.to_sym.in?(RESCUE_FROM_TIMEOUT_ACTIONS)
log_exception(exception)
@timeout = true
- render status: :request_timeout
+
+ if count_action_name?
+ render json: {}, status: :request_timeout
+ else
+ render status: :request_timeout
+ end
+ end
+
+ def count_action_name?
+ action_name.to_sym == :count
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 1c6168dbc2c..e81868faa6e 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -4,7 +4,7 @@ class SnippetsController < Snippets::ApplicationController
include SnippetsActions
include PreviewMarkdown
include ToggleAwardEmoji
- include SpammableActions
+ include SpammableActions::AkismetMarkAsSpamAction
before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam]
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index 4ebf4a80498..37d87baf30b 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -12,17 +12,21 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
def publish(_result = nil)
super
- publish_to_client if should_track? # publish the experiment data to the client
- publish_to_database if @record # publish the experiment context to the database
+ publish_to_client
+ publish_to_database if @record
end
def publish_to_client
+ return unless should_track?
+
Gon.push({ experiment: { name => signature } }, true)
rescue NoMethodError
# means we're not in the request cycle, and can't add to Gon. Log a warning maybe?
end
def publish_to_database
+ return unless should_track?
+
# if the context contains a namespace, group, project, user, or actor
value = context.value
subject = value[:namespace] || value[:group] || value[:project] || value[:user] || value[:actor]
diff --git a/app/experiments/force_company_trial_experiment.rb b/app/experiments/force_company_trial_experiment.rb
new file mode 100644
index 00000000000..00bdd5d693d
--- /dev/null
+++ b/app/experiments/force_company_trial_experiment.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ForceCompanyTrialExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ exclude :setup_for_personal
+
+ private
+
+ def setup_for_personal
+ !context.user.setup_for_company
+ end
+end
diff --git a/app/experiments/members/invite_email_experiment.rb b/app/experiments/members/invite_email_experiment.rb
deleted file mode 100644
index 893061e34f3..00000000000
--- a/app/experiments/members/invite_email_experiment.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-module Members
- class InviteEmailExperiment < ApplicationExperiment
- exclude { context.actor.created_by.blank? }
- exclude { context.actor.created_by.avatar_url.nil? }
-
- 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[activity control]).execute
- end
- end
-
- class RoundRobin
- CacheError = Class.new(StandardError)
-
- COUNTER_EXPIRE_TIME = 86400 # one day
-
- def initialize(key, variants)
- @key = key
- @variants = variants
- end
-
- def execute
- increment_counter
- resolve_variant_name
- end
-
- # When the counter would expire
- #
- # @api private Used internally by SRE and debugging purpose
- # @return [Integer] Number in seconds until expiration or false if never
- def counter_expires_in
- Gitlab::Redis::SharedState.with do |redis|
- redis.ttl(key)
- end
- end
-
- # Return the actual counter value
- #
- # @return [Integer] value
- def counter_value
- Gitlab::Redis::SharedState.with do |redis|
- (redis.get(key) || 0).to_i
- end
- end
-
- # Reset the counter
- #
- # @private Used internally by SRE and debugging purpose
- # @return [Boolean] whether reset was a success
- def reset!
- redis_cmd do |redis|
- redis.del(key)
- end
- end
-
- private
-
- attr_reader :key, :variants
-
- # Increase the counter
- #
- # @return [Boolean] whether operation was a success
- def increment_counter
- redis_cmd do |redis|
- redis.incr(key)
- redis.expire(key, COUNTER_EXPIRE_TIME)
- end
- end
-
- def resolve_variant_name
- remainder = counter_value % variants.size
-
- variants[remainder]
- end
-
- def redis_cmd
- Gitlab::Redis::SharedState.with { |redis| yield(redis) }
-
- true
- rescue CacheError => e
- Gitlab::AppLogger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}")
-
- false
- end
- end
-end
diff --git a/app/experiments/new_project_readme_content_experiment.rb b/app/experiments/new_project_readme_content_experiment.rb
index f86803db093..d9f0fb3b93e 100644
--- a/app/experiments/new_project_readme_content_experiment.rb
+++ b/app/experiments/new_project_readme_content_experiment.rb
@@ -19,7 +19,7 @@ class NewProjectReadmeContentExperiment < ApplicationExperiment # rubocop:disabl
end
def redirect(to_url)
- experiment_redirect_url(self, to_url)
+ experiment_redirect_url(self, url: to_url)
end
private
diff --git a/app/experiments/new_project_readme_experiment.rb b/app/experiments/new_project_readme_experiment.rb
deleted file mode 100644
index c5c41330949..00000000000
--- a/app/experiments/new_project_readme_experiment.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-class NewProjectReadmeExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
- include ProjectCommitCount
-
- INITIAL_WRITE_LIMIT = 3
- EXPERIMENT_START_DATE = DateTime.parse('2021/1/20')
- MAX_ACCOUNT_AGE = 7.days
-
- exclude { context.value[:actor].nil? }
- exclude { context.actor.created_at < MAX_ACCOUNT_AGE.ago }
-
- def control_behavior
- false # we don't want the checkbox to be checked
- end
-
- def candidate_behavior
- true # check the checkbox by default
- end
-
- 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 (count = commit_count(project)) < INITIAL_WRITE_LIMIT
-
- track(:write, property: project.created_at.to_s, value: count)
- end
-
- private
-
- 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 b4f66a38faa..1fbc1a4a258 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -46,7 +46,7 @@ module AlertManagement
def by_status(collection)
values = AlertManagement::Alert.status_names & Array(params[:status])
- values.present? ? collection.for_status(values) : collection
+ values.present? ? collection.with_status(values) : collection
end
def by_search(collection)
diff --git a/app/finders/ci/pipelines_finder.rb b/app/finders/ci/pipelines_finder.rb
index d9fe5c23a7e..a79840216da 100644
--- a/app/finders/ci/pipelines_finder.rb
+++ b/app/finders/ci/pipelines_finder.rb
@@ -29,6 +29,9 @@ module Ci
items = by_username(items)
items = by_yaml_errors(items)
items = by_updated_at(items)
+
+ items = by_source(items) if Feature.enabled?(:pipeline_source_filter, project, default_enabled: :yaml)
+
sort_items(items)
end
@@ -87,6 +90,12 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_source(items)
+ return items unless ::Ci::Pipeline.sources.key?(params[:source])
+
+ items.with_pipeline_source(params[:source])
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_ref(items)
if params[:ref].present?
@@ -107,6 +116,7 @@ module Ci
end
# rubocop: enable CodeReuse/ActiveRecord
+ # This method is deprecated and will be removed in 14.3
# rubocop: disable CodeReuse/ActiveRecord
def by_name(items)
if params[:name].present?
diff --git a/app/finders/error_tracking/errors_finder.rb b/app/finders/error_tracking/errors_finder.rb
new file mode 100644
index 00000000000..fb2d4b14dfa
--- /dev/null
+++ b/app/finders/error_tracking/errors_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorsFinder
+ def initialize(current_user, project, params)
+ @current_user = current_user
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return ErrorTracking::Error.none unless authorized?
+
+ collection = project.error_tracking_errors
+ collection = by_status(collection)
+
+ # Limit collection until pagination implemented
+ collection.limit(20)
+ end
+
+ private
+
+ attr_reader :current_user, :project, :params
+
+ def by_status(collection)
+ if params[:status].present? && ErrorTracking::Error.statuses.key?(params[:status])
+ collection.for_status(params[:status])
+ else
+ collection
+ end
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 982234f7506..75623d33ef5 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -3,6 +3,7 @@
class GroupMembersFinder < UnionFinder
RELATIONS = %i(direct inherited descendants).freeze
DEFAULT_RELATIONS = %i(direct inherited).freeze
+ INVALID_RELATION_TYPE_ERROR_MSG = "is not a valid relation type. Valid relation types are #{RELATIONS.join(', ')}."
RELATIONS_DESCRIPTIONS = {
direct: 'Members in the group itself',
@@ -42,6 +43,8 @@ class GroupMembersFinder < UnionFinder
attr_reader :user, :group
def groups_by_relations(include_relations)
+ check_relation_arguments!(include_relations)
+
case include_relations.sort
when [:inherited]
group.ancestors
@@ -86,6 +89,12 @@ class GroupMembersFinder < UnionFinder
def members_of_groups(groups)
GroupMember.non_request.of_groups(groups)
end
+
+ def check_relation_arguments!(include_relations)
+ unless include_relations & RELATIONS == include_relations
+ raise ArgumentError, "#{(include_relations - RELATIONS).first} #{INVALID_RELATION_TYPE_ERROR_MSG}"
+ end
+ end
end
GroupMembersFinder.prepend_mod_with('GroupMembersFinder')
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 4b6b2716c64..7ea3362fba1 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -11,6 +11,7 @@
# parent: Group
# all_available: boolean (defaults to true)
# min_access_level: integer
+# search: string
# exclude_group_ids: array of integers
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
# filtering by parent. The parent param must be present.
@@ -33,6 +34,7 @@ class GroupsFinder < UnionFinder
item = by_parent(item)
item = by_custom_attributes(item)
item = exclude_group_ids(item)
+ item = by_search(item)
item
end
@@ -94,6 +96,14 @@ class GroupsFinder < UnionFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def by_search(groups)
+ return groups unless params[:search].present?
+
+ groups.search(params[:search], include_parents: params[:parent].blank?)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def owned_groups
current_user&.owned_groups || Group.none
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 086dadcf5b7..9f3ca385d93 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -15,7 +15,8 @@
# state: 'opened' or 'closed' or 'locked' or 'all'
# group_id: integer
# project_id: integer
-# milestone_title: string
+# milestone_title: string (cannot be simultaneously used with milestone_wildcard_id)
+# milestone_wildcard_id: 'none', 'any', 'upcoming', 'started' (cannot be simultaneously used with milestone_title)
# release_tag: string
# author_id: integer
# author_username: string
diff --git a/app/finders/issuable_finder/params.rb b/app/finders/issuable_finder/params.rb
index 51e12dde51d..595f4e4cf8a 100644
--- a/app/finders/issuable_finder/params.rb
+++ b/app/finders/issuable_finder/params.rb
@@ -4,9 +4,11 @@ class IssuableFinder
class Params < SimpleDelegator
include Gitlab::Utils::StrongMemoize
- # This is used as a common filter for None / Any
+ # This is used as a common filter for None / Any / Upcoming / Started
FILTER_NONE = 'none'
FILTER_ANY = 'any'
+ FILTER_STARTED = 'started'
+ FILTER_UPCOMING = 'upcoming'
# This is used in unassigning users
NONE = '0'
@@ -42,25 +44,35 @@ class IssuableFinder
end
def milestones?
- params[:milestone_title].present?
+ params[:milestone_title].present? || params[:milestone_wildcard_id].present?
end
def filter_by_no_milestone?
- # Accepts `No Milestone` for compatibility
- params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
+ # Usage of `No Milestone` and `none`/`None` in milestone_title to be deprecated
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336044
+ params[:milestone_title].to_s.downcase == FILTER_NONE ||
+ params[:milestone_title] == Milestone::None.title ||
+ params[:milestone_wildcard_id].to_s.downcase == FILTER_NONE
end
def filter_by_any_milestone?
- # Accepts `Any Milestone` for compatibility
- params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title
+ # Usage of `Any Milestone` and `any`/`Any` in milestone_title to be deprecated
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336044
+ params[:milestone_title].to_s.downcase == FILTER_ANY ||
+ params[:milestone_title] == Milestone::Any.title ||
+ params[:milestone_wildcard_id].to_s.downcase == FILTER_ANY
end
def filter_by_upcoming_milestone?
- params[:milestone_title] == Milestone::Upcoming.name
+ # Usage of `#upcoming` in milestone_title to be deprecated
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336044
+ params[:milestone_title] == Milestone::Upcoming.name || params[:milestone_wildcard_id].to_s.downcase == FILTER_UPCOMING
end
def filter_by_started_milestone?
- params[:milestone_title] == Milestone::Started.name
+ # Usage of `#started` in milestone_title to be deprecated
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336044
+ params[:milestone_title] == Milestone::Started.name || params[:milestone_wildcard_id].to_s.downcase == FILTER_STARTED
end
def filter_by_no_release?
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 40d6730d232..7595b1c7a15 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -11,7 +11,8 @@
# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
-# milestone_title: string
+# milestone_title: string (cannot be simultaneously used with milestone_wildcard_id)
+# milestone_wildcard_id: 'none', 'any', 'upcoming', 'started' (cannot be simultaneously used with milestone_title)
# assignee_id: integer
# search: string
# in: 'title', 'description', or a string joining them with comma
@@ -25,7 +26,7 @@
# updated_after: datetime
# updated_before: datetime
# confidential: boolean
-# issue_types: array of strings (one of Issue.issue_types)
+# issue_types: array of strings (one of WorkItem::Type.base_types)
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
@@ -46,17 +47,22 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
- return Issue.all if params.user_can_see_all_confidential_issues?
+ return Issue.all if params.user_can_see_all_issues?
+
+ # Only admins can see hidden issues, so for non-admins, we filter out any hidden issues
+ issues = Issue.without_hidden
+
+ return issues.all if params.user_can_see_all_confidential_issues?
# If already filtering by assignee we can skip confidentiality since a user
# can always see confidential issues assigned to them. This is just an
# optimization since a very common usecase of this Finder is to load the
# count of issues assigned to the user for the header bar.
- return Issue.all if current_user && assignee_filter.includes_user?(current_user)
+ return issues.all if current_user && assignee_filter.includes_user?(current_user)
- return Issue.where('issues.confidential IS NOT TRUE') if params.user_cannot_see_confidential_issues?
+ return issues.where('issues.confidential IS NOT TRUE') if params.user_cannot_see_confidential_issues?
- Issue.where('
+ issues.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
@@ -111,7 +117,7 @@ class IssuesFinder < IssuableFinder
def by_issue_types(items)
issue_type_params = Array(params[:issue_types]).map(&:to_s)
return items if issue_type_params.blank?
- return Issue.none unless (Issue.issue_types.keys & issue_type_params).sort == issue_type_params.sort
+ return Issue.none unless (WorkItem::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
items.with_issue_type(params[:issue_types])
end
diff --git a/app/finders/issues_finder/params.rb b/app/finders/issues_finder/params.rb
index 1de117216f8..2edd8a6f099 100644
--- a/app/finders/issues_finder/params.rb
+++ b/app/finders/issues_finder/params.rb
@@ -32,7 +32,7 @@ class IssuesFinder
if parent
Ability.allowed?(current_user, :read_confidential_issues, parent)
else
- Ability.allowed?(current_user, :read_all_resources)
+ user_can_see_all_issues?
end
end
end
@@ -42,6 +42,12 @@ class IssuesFinder
current_user.blank?
end
+
+ def user_can_see_all_issues?
+ strong_memoize(:user_can_see_all_issues) do
+ Ability.allowed?(current_user, :read_all_resources)
+ end
+ end
end
end
diff --git a/app/finders/lfs_pointers_finder.rb b/app/finders/lfs_pointers_finder.rb
new file mode 100644
index 00000000000..7e621002f1e
--- /dev/null
+++ b/app/finders/lfs_pointers_finder.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class LfsPointersFinder
+ def initialize(repository, path)
+ @repository = repository
+ @path = path
+ end
+
+ def execute
+ return [] unless ref
+
+ blob_ids = tree.blobs.map(&:id)
+
+ # When current endpoint is a Blob then `tree.blobs` will be empty, it means we need to analyze
+ # the current Blob in order to determine if it's a LFS object
+ blob_ids = Array.wrap(current_blob&.id) if blob_ids.empty?
+
+ Gitlab::Git::Blob.batch_lfs_pointers(repository, blob_ids).map(&:id)
+ end
+
+ private
+
+ attr_reader :repository, :path
+
+ def ref
+ repository.root_ref
+ end
+
+ def tree
+ repository.tree(ref, path)
+ end
+
+ def current_blob
+ repository.blob_at(ref, path)
+ end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index e23fa3f7f68..13696add965 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -154,16 +154,9 @@ class MergeRequestsFinder < IssuableFinder
# WIP is deprecated in favor of Draft. Currently both options are supported
def wip_match(table)
- items =
- table[:title].matches('WIP:%')
- .or(table[:title].matches('WIP %'))
- .or(table[:title].matches('[WIP]%'))
-
- # Let's keep this FF around until https://gitlab.com/gitlab-org/gitlab/-/issues/232999
- # is implemented
- return items unless Feature.enabled?(:merge_request_draft_filter, default_enabled: true)
-
- items
+ table[:title].matches('WIP:%')
+ .or(table[:title].matches('WIP %'))
+ .or(table[:title].matches('[WIP]%'))
.or(table[:title].matches('Draft - %'))
.or(table[:title].matches('Draft:%'))
.or(table[:title].matches('[Draft]%'))
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 642ca2cf2e6..47cfb59944b 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -3,11 +3,8 @@
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
+ def execute
+ packages.with_normalized_pypi_name(@params[:package_name])
end
private
diff --git a/app/finders/projects/members/effective_access_level_finder.rb b/app/finders/projects/members/effective_access_level_finder.rb
index 2880d6667ce..c1e3842a9e4 100644
--- a/app/finders/projects/members/effective_access_level_finder.rb
+++ b/app/finders/projects/members/effective_access_level_finder.rb
@@ -59,8 +59,8 @@ module Projects
# @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
+ all_possible_avenues_of_membership.flat_map do |members|
+ apply_scopes(members).pluck(*USER_ID_AND_ACCESS_LEVEL) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
@@ -86,7 +86,7 @@ module Projects
members << Member.from_union(members_per_batch)
end
- members.flatten
+ Member.from_union(members)
end
def project_owner_acting_as_maintainer
@@ -120,6 +120,10 @@ module Projects
Arel.sql(column_alias)
)
end
+
+ def apply_scopes(members)
+ members
+ end
end
end
end
diff --git a/app/finders/projects/members/effective_access_level_per_user_finder.rb b/app/finders/projects/members/effective_access_level_per_user_finder.rb
new file mode 100644
index 00000000000..f367b4ca451
--- /dev/null
+++ b/app/finders/projects/members/effective_access_level_per_user_finder.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Projects
+ module Members
+ class EffectiveAccessLevelPerUserFinder < EffectiveAccessLevelFinder
+ def initialize(project, user)
+ @project = project
+ @user = user
+ end
+
+ private
+
+ attr_reader :user
+
+ def apply_scopes(members)
+ super.where(user_id: user.id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/app/finders/security/jobs_finder.rb b/app/finders/security/jobs_finder.rb
index 99bcf97f43c..6c2090e0509 100644
--- a/app/finders/security/jobs_finder.rb
+++ b/app/finders/security/jobs_finder.rb
@@ -38,11 +38,7 @@ module Security
def execute
return [] if @job_types.empty?
- if Feature.enabled?(:ci_build_metadata_config, pipeline.project, default_enabled: :yaml)
- find_jobs
- else
- find_jobs_legacy
- end
+ find_jobs
end
private
@@ -51,19 +47,6 @@ module Security
@pipeline.builds.with_secure_reports_from_config_options(@job_types)
end
- def find_jobs_legacy
- # the query doesn't guarantee accuracy, so we verify it here
- legacy_jobs_query.select do |job|
- @job_types.find { |job_type| job.options.dig(:artifacts, :reports, job_type) }
- end
- end
-
- def legacy_jobs_query
- @job_types.map do |job_type|
- @pipeline.builds.with_secure_reports_from_options(job_type)
- end.reduce(&:or)
- end
-
def valid_job_types?(job_types)
(job_types - self.class.allowed_job_types).empty?
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 8e95bd501ff..38ba1611c48 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -3,9 +3,9 @@
class GitlabSchema < GraphQL::Schema
# Currently an IntrospectionQuery has a complexity of 179.
# These values will evolve over time.
- DEFAULT_MAX_COMPLEXITY = 200
- AUTHENTICATED_COMPLEXITY = 250
- ADMIN_COMPLEXITY = 300
+ DEFAULT_MAX_COMPLEXITY = 200
+ AUTHENTICATED_MAX_COMPLEXITY = 250
+ ADMIN_MAX_COMPLEXITY = 300
DEFAULT_MAX_DEPTH = 15
AUTHENTICATED_MAX_DEPTH = 20
@@ -20,9 +20,6 @@ class GitlabSchema < GraphQL::Schema
query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
query_analyzer Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer.new
- max_complexity DEFAULT_MAX_COMPLEXITY
- max_depth DEFAULT_MAX_DEPTH
-
query Types::QueryType
mutation Types::MutationType
subscription Types::SubscriptionType
@@ -36,20 +33,13 @@ class GitlabSchema < GraphQL::Schema
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless kwargs.key?(:max_complexity)
queries.each do |query|
- query[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless query.key?(:max_complexity)
- query[:max_depth] = max_query_depth(kwargs[:context])
+ query[:max_complexity] ||= max_query_complexity(query[:context]) unless query.key?(:max_complexity)
+ query[:max_depth] = max_query_depth(query[:context]) unless query.key?(:max_depth)
end
super(queries, **kwargs)
end
- def execute(query_str = nil, **kwargs)
- kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
- kwargs[:max_depth] ||= max_query_depth(kwargs[:context])
-
- super(query_str, **kwargs)
- end
-
def get_type(type_name)
type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
@@ -142,9 +132,9 @@ class GitlabSchema < GraphQL::Schema
current_user = ctx&.fetch(:current_user, nil)
if current_user&.admin
- ADMIN_COMPLEXITY
+ ADMIN_MAX_COMPLEXITY
elsif current_user
- AUTHENTICATED_COMPLEXITY
+ AUTHENTICATED_MAX_COMPLEXITY
else
DEFAULT_MAX_COMPLEXITY
end
diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
index ea1502d4b62..d943816089f 100644
--- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
+++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
@@ -10,15 +10,15 @@ module Mutations
Gitlab::ApplicationContext::KNOWN_KEYS.each do |key|
argument key,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: "Delete jobs matching #{key} in the context metadata"
end
argument :queue_name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
- description: 'The name of the queue to delete jobs from.'
+ description: 'Name of the queue to delete jobs from.'
field :result,
Types::Admin::SidekiqQueues::DeleteJobsResponseType,
diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
index 517c20a85d0..c986111d290 100644
--- a/app/graphql/mutations/alert_management/alerts/set_assignees.rb
+++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
@@ -7,14 +7,14 @@ module Mutations
graphql_name 'AlertSetAssignees'
argument :assignee_usernames,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
required: true,
- description: 'The usernames to assign to the alert. Replaces existing assignees by default.'
+ description: 'Usernames to assign to the alert. Replaces existing assignees by default.'
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
- description: 'The operation to perform. Defaults to REPLACE.'
+ description: 'Operation to perform. Defaults to REPLACE.'
def resolve(args)
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb
index 86908c1449c..d01f200107c 100644
--- a/app/graphql/mutations/alert_management/base.rb
+++ b/app/graphql/mutations/alert_management/base.rb
@@ -5,28 +5,28 @@ module Mutations
class Base < BaseMutation
include Gitlab::Utils::UsageData
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project the alert to mutate is in."
+ description: "Project the alert to mutate is in."
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
- description: "The IID of the alert to mutate."
+ description: "IID of the alert to mutate."
field :alert,
Types::AlertManagement::AlertType,
null: true,
- description: "The alert after mutation."
+ description: "Alert after mutation."
field :todo,
Types::TodoType,
null: true,
- description: "The to-do item after mutation."
+ description: "To-do item after mutation."
field :issue,
Types::IssueType,
null: true,
- description: "The issue created after mutation."
+ description: "Issue created after mutation."
authorize :update_alert_management_alert
diff --git a/app/graphql/mutations/alert_management/http_integration/create.rb b/app/graphql/mutations/alert_management/http_integration/create.rb
index 54803855bcf..04840ac43bd 100644
--- a/app/graphql/mutations/alert_management/http_integration/create.rb
+++ b/app/graphql/mutations/alert_management/http_integration/create.rb
@@ -8,15 +8,15 @@ module Mutations
graphql_name 'HttpIntegrationCreate'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to create the integration in.'
+ description: 'Project to create the integration in.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: true,
- description: 'The name of the integration.'
+ description: 'Name of the integration.'
- argument :active, GraphQL::BOOLEAN_TYPE,
+ argument :active, GraphQL::Types::Boolean,
required: true,
description: 'Whether the integration is receiving alerts.'
diff --git a/app/graphql/mutations/alert_management/http_integration/destroy.rb b/app/graphql/mutations/alert_management/http_integration/destroy.rb
index d0420e2bcb5..dc5c73ecff6 100644
--- a/app/graphql/mutations/alert_management/http_integration/destroy.rb
+++ b/app/graphql/mutations/alert_management/http_integration/destroy.rb
@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
- description: "The ID of the integration to remove."
+ description: "ID of the integration to remove."
def resolve(id:)
integration = authorized_find!(id: id)
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 efa92bfe895..2f25d315d2e 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
@@ -7,7 +7,7 @@ module Mutations
field :integration,
Types::AlertManagement::HttpIntegrationType,
null: true,
- description: "The HTTP integration."
+ description: "HTTP integration."
authorize :admin_operations
diff --git a/app/graphql/mutations/alert_management/http_integration/reset_token.rb b/app/graphql/mutations/alert_management/http_integration/reset_token.rb
index bf73a9eaae7..83ad7762408 100644
--- a/app/graphql/mutations/alert_management/http_integration/reset_token.rb
+++ b/app/graphql/mutations/alert_management/http_integration/reset_token.rb
@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
- description: "The ID of the integration to mutate."
+ description: "ID of the integration to mutate."
def resolve(id:)
integration = authorized_find!(id: id)
diff --git a/app/graphql/mutations/alert_management/http_integration/update.rb b/app/graphql/mutations/alert_management/http_integration/update.rb
index 4e6e7995c10..78424e317b8 100644
--- a/app/graphql/mutations/alert_management/http_integration/update.rb
+++ b/app/graphql/mutations/alert_management/http_integration/update.rb
@@ -8,13 +8,13 @@ module Mutations
argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
required: true,
- description: "The ID of the integration to mutate."
+ description: "ID of the integration to mutate."
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
- description: "The name of the integration."
+ description: "Name of the integration."
- argument :active, GraphQL::BOOLEAN_TYPE,
+ argument :active, GraphQL::Types::Boolean,
required: false,
description: "Whether the integration is receiving alerts."
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb
index 4d0a5a5cb13..0153bd0e42a 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/create.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb
@@ -8,15 +8,15 @@ module Mutations
graphql_name 'PrometheusIntegrationCreate'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to create the integration in.'
+ description: 'Project to create the integration in.'
- argument :active, GraphQL::BOOLEAN_TYPE,
+ argument :active, GraphQL::Types::Boolean,
required: true,
description: 'Whether the integration is receiving alerts.'
- argument :api_url, GraphQL::STRING_TYPE,
+ argument :api_url, GraphQL::Types::String,
required: true,
description: 'Endpoint at which Prometheus can be queried.'
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb
index d8678ea4d61..29834d63f35 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb
@@ -7,7 +7,7 @@ module Mutations
field :integration,
Types::AlertManagement::PrometheusIntegrationType,
null: true,
- description: "The newly created integration."
+ description: "Newly created integration."
authorize :admin_project
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb
index 33a12405583..71c02efdc03 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb
@@ -8,7 +8,7 @@ module Mutations
argument :id, Types::GlobalIDType[::Integrations::Prometheus],
required: true,
- description: "The ID of the integration to mutate."
+ description: "ID of the integration to mutate."
def resolve(id:)
integration = authorized_find!(id: id)
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb
index ddab1af908c..50aafdc26a6 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/update.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb
@@ -8,13 +8,13 @@ module Mutations
argument :id, Types::GlobalIDType[::Integrations::Prometheus],
required: true,
- description: "The ID of the integration to mutate."
+ description: "ID of the integration to mutate."
- argument :active, GraphQL::BOOLEAN_TYPE,
+ argument :active, GraphQL::Types::Boolean,
required: false,
description: "Whether the integration is receiving alerts."
- argument :api_url, GraphQL::STRING_TYPE,
+ argument :api_url, GraphQL::Types::String,
required: false,
description: "Endpoint at which Prometheus can be queried."
diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb
index 67f8ba9118f..21566c7d66f 100644
--- a/app/graphql/mutations/alert_management/update_alert_status.rb
+++ b/app/graphql/mutations/alert_management/update_alert_status.rb
@@ -7,7 +7,7 @@ module Mutations
argument :status, Types::AlertManagement::StatusEnum,
required: true,
- description: 'The status to set the alert.'
+ description: 'Status to set the alert.'
def resolve(project_path:, iid:, status:)
alert = authorized_find!(project_path: project_path, iid: iid)
diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb
index 4f41a9cafd8..ce73f2083f2 100644
--- a/app/graphql/mutations/award_emojis/base.rb
+++ b/app/graphql/mutations/award_emojis/base.rb
@@ -12,17 +12,17 @@ module Mutations
argument :awardable_id,
::Types::GlobalIDType[::Awardable],
required: true,
- description: 'The global ID of the awardable resource.'
+ description: 'Global ID of the awardable resource.'
argument :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
description: copy_field_description(Types::AwardEmojis::AwardEmojiType, :name)
field :award_emoji,
Types::AwardEmojis::AwardEmojiType,
null: true,
- description: 'The award emoji after mutation.'
+ description: 'Award emoji after mutation.'
private
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
index e741f972b1b..5da2731d562 100644
--- a/app/graphql/mutations/award_emojis/toggle.rb
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -5,7 +5,7 @@ module Mutations
class Toggle < Base
graphql_name 'AwardEmojiToggle'
- field :toggled_on, GraphQL::BOOLEAN_TYPE, null: false,
+ field :toggled_on, GraphQL::Types::Boolean, null: false,
description: 'Indicates the status of the emoji. ' \
'True if the toggle awarded the emoji, and false if the toggle removed the emoji.'
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index da658e1f108..5de042f78d6 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -11,7 +11,7 @@ module Mutations
field_class ::Types::BaseField
argument_class ::Types::BaseArgument
- field :errors, [GraphQL::STRING_TYPE],
+ field :errors, [GraphQL::Types::String],
null: false,
description: 'Errors encountered during execution of the mutation.'
@@ -31,6 +31,12 @@ module Mutations
def ready?(**args)
raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only?
+ missing_args = self.class.arguments.values
+ .reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) }
+ .map(&:graphql_name)
+
+ raise ArgumentError, "Arguments must be provided: #{missing_args.join(", ")}" if missing_args.any?
+
true
end
diff --git a/app/graphql/mutations/boards/common_mutation_arguments.rb b/app/graphql/mutations/boards/common_mutation_arguments.rb
index c4f8d299318..aedb7c9c725 100644
--- a/app/graphql/mutations/boards/common_mutation_arguments.rb
+++ b/app/graphql/mutations/boards/common_mutation_arguments.rb
@@ -7,15 +7,15 @@ module Mutations
included do
argument :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
- description: 'The board name.'
+ description: 'Board name.'
argument :hide_backlog_list,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::BoardType, :hide_backlog_list)
argument :hide_closed_list,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::BoardType, :hide_closed_list)
end
diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb
index 44fc22cf883..080bf7c6e79 100644
--- a/app/graphql/mutations/boards/create.rb
+++ b/app/graphql/mutations/boards/create.rb
@@ -12,7 +12,7 @@ module Mutations
field :board,
Types::BoardType,
null: true,
- description: 'The board after mutation.'
+ description: 'Board after mutation.'
authorize :admin_issue_board
diff --git a/app/graphql/mutations/boards/destroy.rb b/app/graphql/mutations/boards/destroy.rb
index 4a0068edee2..61e0c95f8d3 100644
--- a/app/graphql/mutations/boards/destroy.rb
+++ b/app/graphql/mutations/boards/destroy.rb
@@ -8,11 +8,11 @@ module Mutations
field :board,
Types::BoardType,
null: true,
- description: 'The board after mutation.'
+ description: 'Board after mutation.'
argument :id,
::Types::GlobalIDType[::Board],
required: true,
- description: 'The global ID of the board to destroy.'
+ description: 'Global ID of the board to destroy.'
authorize :admin_issue_board
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index b73657ea0c8..14fe9714f99 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -6,19 +6,19 @@ module Mutations
class IssueMoveList < Mutations::Issues::Base
graphql_name 'IssueMoveList'
BoardGID = ::Types::GlobalIDType[::Board]
- ListID = ::GraphQL::ID_TYPE
- IssueID = ::GraphQL::ID_TYPE
+ ListID = ::GraphQL::Types::ID
+ IssueID = ::GraphQL::Types::ID
argument :board_id, BoardGID,
required: true,
loads: Types::BoardType,
description: 'Global ID of the board that the issue is in.'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project the issue to mutate is in.'
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
description: 'IID of the issue to mutate.'
@@ -56,11 +56,11 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
- move_issue(board, issue, move_params)
+ result = move_issue(board, issue, move_params)
{
issue: issue.reset,
- errors: issue.errors.full_messages
+ errors: error_for(result)
}
end
@@ -79,6 +79,12 @@ module Mutations
def move_arguments(args)
args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
end
+
+ def error_for(result)
+ return [] unless result.error?
+
+ [result.message]
+ end
end
end
end
diff --git a/app/graphql/mutations/boards/lists/base_create.rb b/app/graphql/mutations/boards/lists/base_create.rb
index a21c7feece3..a2d849848e0 100644
--- a/app/graphql/mutations/boards/lists/base_create.rb
+++ b/app/graphql/mutations/boards/lists/base_create.rb
@@ -4,7 +4,7 @@ module Mutations
module Boards
module Lists
class BaseCreate < BaseMutation
- argument :backlog, GraphQL::BOOLEAN_TYPE,
+ argument :backlog, GraphQL::Types::Boolean,
required: false,
description: 'Create the backlog list.'
diff --git a/app/graphql/mutations/boards/lists/base_update.rb b/app/graphql/mutations/boards/lists/base_update.rb
index b06cb3b1e32..7962d9c85d4 100644
--- a/app/graphql/mutations/boards/lists/base_update.rb
+++ b/app/graphql/mutations/boards/lists/base_update.rb
@@ -4,11 +4,11 @@ module Mutations
module Boards
module Lists
class BaseUpdate < BaseMutation
- argument :position, GraphQL::INT_TYPE,
+ argument :position, GraphQL::Types::Int,
required: false,
description: 'Position of list within the board.'
- argument :collapsed, GraphQL::BOOLEAN_TYPE,
+ argument :collapsed, GraphQL::Types::Boolean,
required: false,
description: 'Indicates if the list is collapsed for this user.'
diff --git a/app/graphql/mutations/boards/lists/destroy.rb b/app/graphql/mutations/boards/lists/destroy.rb
index a50b5f73455..4ffb95abe64 100644
--- a/app/graphql/mutations/boards/lists/destroy.rb
+++ b/app/graphql/mutations/boards/lists/destroy.rb
@@ -9,7 +9,7 @@ module Mutations
field :list,
Types::BoardListType,
null: true,
- description: 'The list after mutation.'
+ description: 'List after mutation.'
argument :list_id, ::Types::GlobalIDType[::List],
required: true,
diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb
index f1a1d57306b..40ab5a5ba7d 100644
--- a/app/graphql/mutations/boards/update.rb
+++ b/app/graphql/mutations/boards/update.rb
@@ -10,12 +10,12 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Board],
required: true,
- description: 'The board global ID.'
+ description: 'Board global ID.'
field :board,
Types::BoardType,
null: true,
- description: 'The board after mutation.'
+ description: 'Board after mutation.'
authorize :admin_issue_board
diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb
index a94d3966258..078c84bcdc0 100644
--- a/app/graphql/mutations/branches/create.rb
+++ b/app/graphql/mutations/branches/create.rb
@@ -7,16 +7,16 @@ module Mutations
graphql_name 'CreateBranch'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the branch is associated with.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: true,
description: 'Name of the branch.'
argument :ref,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
description: 'Branch name or commit SHA to create branch from.'
diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb
index 0973e9beae3..7bd38bc2998 100644
--- a/app/graphql/mutations/ci/ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb
@@ -9,22 +9,22 @@ module Mutations
authorize :admin_project
- argument :full_path, GraphQL::ID_TYPE,
+ argument :full_path, GraphQL::Types::ID,
required: true,
description: 'Full Path of the project the settings belong to.'
- argument :keep_latest_artifact, GraphQL::BOOLEAN_TYPE,
+ argument :keep_latest_artifact, GraphQL::Types::Boolean,
required: false,
description: 'Indicates if the latest artifact should be kept for this project.'
- argument :job_token_scope_enabled, GraphQL::BOOLEAN_TYPE,
+ argument :job_token_scope_enabled, GraphQL::Types::Boolean,
required: false,
description: 'Indicates CI job tokens generated in this project have restricted access to resources.'
field :ci_cd_settings,
Types::Ci::CiCdSettingType,
null: false,
- description: 'The CI/CD settings after mutation.'
+ description: 'CI/CD settings after mutation.'
def resolve(full_path:, **args)
project = authorized_find!(full_path)
diff --git a/app/graphql/mutations/ci/job/base.rb b/app/graphql/mutations/ci/job/base.rb
index 3359def159a..a9fe26226d9 100644
--- a/app/graphql/mutations/ci/job/base.rb
+++ b/app/graphql/mutations/ci/job/base.rb
@@ -8,7 +8,7 @@ module Mutations
argument :id, JobID,
required: true,
- description: 'The ID of the job to mutate.'
+ description: 'ID of the job to mutate.'
def find_object(id: )
# TODO: remove this line when the compatibility layer is removed
diff --git a/app/graphql/mutations/ci/job/cancel.rb b/app/graphql/mutations/ci/job/cancel.rb
new file mode 100644
index 00000000000..dc9f4d19779
--- /dev/null
+++ b/app/graphql/mutations/ci/job/cancel.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Cancel < Base
+ graphql_name 'JobCancel'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'Job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+
+ ::Ci::BuildCancelService.new(job, current_user).execute
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/play.rb b/app/graphql/mutations/ci/job/play.rb
index f87904f8b25..99f62ea3e70 100644
--- a/app/graphql/mutations/ci/job/play.rb
+++ b/app/graphql/mutations/ci/job/play.rb
@@ -9,7 +9,7 @@ module Mutations
field :job,
Types::Ci::JobType,
null: true,
- description: 'The job after the mutation.'
+ description: 'Job after the mutation.'
authorize :update_build
diff --git a/app/graphql/mutations/ci/job/retry.rb b/app/graphql/mutations/ci/job/retry.rb
index a61d5dddb40..9af357ab216 100644
--- a/app/graphql/mutations/ci/job/retry.rb
+++ b/app/graphql/mutations/ci/job/retry.rb
@@ -9,7 +9,7 @@ module Mutations
field :job,
Types::Ci::JobType,
null: true,
- description: 'The job after the mutation.'
+ description: 'Job after the mutation.'
authorize :update_build
diff --git a/app/graphql/mutations/ci/job/unschedule.rb b/app/graphql/mutations/ci/job/unschedule.rb
new file mode 100644
index 00000000000..07b1896bd2c
--- /dev/null
+++ b/app/graphql/mutations/ci/job/unschedule.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Unschedule < Base
+ graphql_name 'JobUnschedule'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'Job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+
+ ::Ci::BuildUnscheduleService.new(job, current_user).execute
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job_token_scope/add_project.rb b/app/graphql/mutations/ci/job_token_scope/add_project.rb
index 30f98a537b5..41adcae2c82 100644
--- a/app/graphql/mutations/ci/job_token_scope/add_project.rb
+++ b/app/graphql/mutations/ci/job_token_scope/add_project.rb
@@ -10,18 +10,18 @@ module Mutations
authorize :admin_project
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project that the CI job token scope belongs to.'
+ description: 'Project that the CI job token scope belongs to.'
- argument :target_project_path, GraphQL::ID_TYPE,
+ argument :target_project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to be added to the CI job token scope.'
+ description: 'Project to be added to the CI job token scope.'
field :ci_job_token_scope,
Types::Ci::JobTokenScopeType,
null: true,
- description: "The CI job token's scope of access."
+ description: "CI job token's scope of access."
def resolve(project_path:, target_project_path:)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/ci/job_token_scope/remove_project.rb b/app/graphql/mutations/ci/job_token_scope/remove_project.rb
index 71c9083bef8..dd6b2358dd5 100644
--- a/app/graphql/mutations/ci/job_token_scope/remove_project.rb
+++ b/app/graphql/mutations/ci/job_token_scope/remove_project.rb
@@ -10,18 +10,18 @@ module Mutations
authorize :admin_project
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project that the CI job token scope belongs to.'
+ description: 'Project that the CI job token scope belongs to.'
- argument :target_project_path, GraphQL::ID_TYPE,
+ argument :target_project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to be removed from the CI job token scope.'
+ description: 'Project to be removed from the CI job token scope.'
field :ci_job_token_scope,
Types::Ci::JobTokenScopeType,
null: true,
- description: "The CI job token's scope of access."
+ description: "CI job token's scope of access."
def resolve(project_path:, target_project_path:)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/ci/pipeline/base.rb b/app/graphql/mutations/ci/pipeline/base.rb
index ebfab56e743..aed8035a52a 100644
--- a/app/graphql/mutations/ci/pipeline/base.rb
+++ b/app/graphql/mutations/ci/pipeline/base.rb
@@ -8,7 +8,7 @@ module Mutations
argument :id, PipelineID,
required: true,
- description: 'The ID of the pipeline to mutate.'
+ description: 'ID of the pipeline to mutate.'
private
diff --git a/app/graphql/mutations/ci/pipeline/retry.rb b/app/graphql/mutations/ci/pipeline/retry.rb
index a12330470f0..ee93f99703e 100644
--- a/app/graphql/mutations/ci/pipeline/retry.rb
+++ b/app/graphql/mutations/ci/pipeline/retry.rb
@@ -9,7 +9,7 @@ module Mutations
field :pipeline,
Types::Ci::PipelineType,
null: true,
- description: 'The pipeline after mutation.'
+ description: 'Pipeline after mutation.'
authorize :update_pipeline
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index 4cdfa1fb1bd..e37ab1081f9 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -14,11 +14,11 @@ module Mutations
required: true,
description: 'ID of the runner to update.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description of the runner.'
- argument :maximum_timeout, GraphQL::INT_TYPE,
+ argument :maximum_timeout, GraphQL::Types::Int,
required: false,
description: 'Maximum timeout (in seconds) for jobs processed by the runner.'
@@ -26,24 +26,24 @@ module Mutations
required: false,
description: 'Access level of the runner.'
- argument :active, GraphQL::BOOLEAN_TYPE,
+ argument :active, GraphQL::Types::Boolean,
required: false,
description: 'Indicates the runner is allowed to receive jobs.'
- argument :locked, GraphQL::BOOLEAN_TYPE, required: false,
+ argument :locked, GraphQL::Types::Boolean, required: false,
description: 'Indicates the runner is locked.'
- argument :run_untagged, GraphQL::BOOLEAN_TYPE,
+ argument :run_untagged, GraphQL::Types::Boolean,
required: false,
description: 'Indicates the runner is able to run untagged jobs.'
- argument :tag_list, [GraphQL::STRING_TYPE], required: false,
+ argument :tag_list, [GraphQL::Types::String], required: false,
description: 'Tags associated with the runner.'
field :runner,
Types::Ci::RunnerType,
null: true,
- description: 'The runner after mutation.'
+ description: 'Runner after mutation.'
def resolve(id:, **runner_attrs)
runner = authorized_find!(id)
diff --git a/app/graphql/mutations/ci/runners_registration_token/reset.rb b/app/graphql/mutations/ci/runners_registration_token/reset.rb
index e1cdd9a22a5..7976e8fb70d 100644
--- a/app/graphql/mutations/ci/runners_registration_token/reset.rb
+++ b/app/graphql/mutations/ci/runners_registration_token/reset.rb
@@ -8,7 +8,7 @@ module Mutations
authorize :update_runners_registration_token
- ScopeID = ::GraphQL::ID_TYPE
+ ScopeID = ::GraphQL::Types::ID
argument :type, ::Types::Ci::RunnerTypeEnum,
required: true,
@@ -19,9 +19,9 @@ module Mutations
description: 'ID of the project or group to reset the token for. Omit if resetting instance runner token.'
field :token,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
- description: 'The runner token after mutation.'
+ description: 'Runner token after mutation.'
def resolve(**args)
{
diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb
index f432f679909..3eb1912dbc4 100644
--- a/app/graphql/mutations/commits/create.rb
+++ b/app/graphql/mutations/commits/create.rb
@@ -12,20 +12,20 @@ module Mutations
graphql_name 'CommitCreate'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the branch is associated with.'
- argument :branch, GraphQL::STRING_TYPE,
+ argument :branch, GraphQL::Types::String,
required: true,
description: 'Name of the branch to commit into, it can be a new branch.'
- argument :start_branch, GraphQL::STRING_TYPE,
+ argument :start_branch, GraphQL::Types::String,
required: false,
description: 'If on a new branch, name of the original branch.'
argument :message,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
description: copy_field_description(Types::CommitType, :message)
@@ -35,17 +35,17 @@ module Mutations
description: 'Array of action hashes to commit as a batch.'
field :commit_pipeline_path,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: "ETag path for the commit's pipeline."
field :commit,
Types::CommitType,
null: true,
- description: 'The commit after mutation.'
+ description: 'Commit after mutation.'
field :content,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
null: true,
description: 'Contents of the commit.'
diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb
index e214a57500c..86f37207a2d 100644
--- a/app/graphql/mutations/concerns/mutations/assignable.rb
+++ b/app/graphql/mutations/concerns/mutations/assignable.rb
@@ -6,15 +6,15 @@ module Mutations
included do
argument :assignee_usernames,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
required: true,
- description: 'The usernames to assign to the resource. Replaces existing assignees by default.'
+ description: 'Usernames to assign to the resource. Replaces existing assignees by default.'
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
default_value: Types::MutationOperationModeEnum.default_mode,
- description: 'The operation to perform. Defaults to REPLACE.'
+ description: 'Operation to perform. Defaults to REPLACE.'
end
def resolve(project_path:, iid:, assignee_usernames:, operation_mode:)
diff --git a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
index 3c5f077110c..f1ae54aa014 100644
--- a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
+++ b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
@@ -13,39 +13,39 @@ module Mutations
}.freeze
included do
- argument :captcha_response, GraphQL::STRING_TYPE,
+ argument :captcha_response, GraphQL::Types::String,
required: false,
deprecated: DEPRECATION_NOTICE,
- description: 'A valid CAPTCHA response value obtained by using the provided captchaSiteKey with a CAPTCHA API to present a challenge to be solved on the client. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
+ description: 'Valid CAPTCHA response value obtained by using the provided captchaSiteKey with a CAPTCHA API to present a challenge to be solved on the client. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
- argument :spam_log_id, GraphQL::INT_TYPE,
+ argument :spam_log_id, GraphQL::Types::Int,
required: false,
deprecated: DEPRECATION_NOTICE,
- description: 'The spam log ID which must be passed along with a valid CAPTCHA response for the operation to be completed. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
+ description: 'Spam log ID which must be passed along with a valid CAPTCHA response for the operation to be completed. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
field :spam,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
null: true,
deprecated: DEPRECATION_NOTICE,
description: 'Indicates whether the operation was detected as definite spam. There is no option to resubmit the request with a CAPTCHA response.'
field :needs_captcha_response,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
null: true,
deprecated: DEPRECATION_NOTICE,
description: 'Indicates whether the operation was detected as possible spam and not completed. If CAPTCHA is enabled, the request must be resubmitted with a valid CAPTCHA response and spam_log_id included for the operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
field :spam_log_id,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
deprecated: DEPRECATION_NOTICE,
- description: 'The spam log ID which must be passed along with a valid CAPTCHA response for an operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
+ description: 'Spam log ID which must be passed along with a valid CAPTCHA response for an operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
field :captcha_site_key,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
deprecated: DEPRECATION_NOTICE,
- description: 'The CAPTCHA site key which must be used to render a challenge for the user to solve to obtain a valid captchaResponse value. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
+ description: 'CAPTCHA site key which must be used to render a challenge for the user to solve to obtain a valid captchaResponse value. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
end
end
end
diff --git a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb
index b8ef675c3d4..a05884f189c 100644
--- a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb
+++ b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb
@@ -7,11 +7,11 @@ module Mutations
include ResolvesProject
included do
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: false,
description: 'Full path of the project with which the resource is associated.'
- argument :group_path, GraphQL::ID_TYPE,
+ argument :group_path, GraphQL::Types::ID,
required: false,
description: 'Full path of the group with which the resource is associated.'
end
diff --git a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb
index ed9fb5fceb0..1c9e8f311e4 100644
--- a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb
+++ b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb
@@ -6,9 +6,9 @@ module Mutations
included do
argument :subscribed_state,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
- description: 'The desired state of the subscription.'
+ description: 'Desired state of the subscription.'
end
def resolve(project_path:, iid:, subscribed_state:)
diff --git a/app/graphql/mutations/container_expiration_policies/update.rb b/app/graphql/mutations/container_expiration_policies/update.rb
index f61d852bb6c..db4acadfc38 100644
--- a/app/graphql/mutations/container_expiration_policies/update.rb
+++ b/app/graphql/mutations/container_expiration_policies/update.rb
@@ -10,12 +10,12 @@ module Mutations
authorize :destroy_container_image
argument :project_path,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
required: true,
- description: 'The project path where the container expiration policy is located.'
+ description: 'Project path where the container expiration policy is located.'
argument :enabled,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::ContainerExpirationPolicyType, :enabled)
@@ -47,7 +47,7 @@ module Mutations
field :container_expiration_policy,
Types::ContainerExpirationPolicyType,
null: true,
- description: 'The container expiration policy after mutation.'
+ description: 'Container expiration policy after mutation.'
def resolve(project_path:, **args)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/container_repositories/destroy.rb b/app/graphql/mutations/container_repositories/destroy.rb
index 90fba66e7b3..1d8f7b22f88 100644
--- a/app/graphql/mutations/container_repositories/destroy.rb
+++ b/app/graphql/mutations/container_repositories/destroy.rb
@@ -15,7 +15,7 @@ module Mutations
field :container_repository,
Types::ContainerRepositoryType,
null: false,
- description: 'The container repository policy after scheduling the deletion.'
+ description: 'Container repository policy after scheduling the deletion.'
def resolve(id:)
container_repository = authorized_find!(id: id)
diff --git a/app/graphql/mutations/container_repositories/destroy_tags.rb b/app/graphql/mutations/container_repositories/destroy_tags.rb
index 12d65f604b8..c2737820d22 100644
--- a/app/graphql/mutations/container_repositories/destroy_tags.rb
+++ b/app/graphql/mutations/container_repositories/destroy_tags.rb
@@ -17,7 +17,7 @@ module Mutations
description: 'ID of the container repository.'
argument :tag_names,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
required: true,
description: "Container repository tag(s) to delete. Total number can't be greater than #{LIMIT}",
prepare: ->(tag_names, _) do
@@ -27,7 +27,7 @@ module Mutations
end
field :deleted_tag_names,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
description: 'Deleted container repository tags.',
null: false
diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb
index 5cf54f8f877..ad392d6c814 100644
--- a/app/graphql/mutations/custom_emoji/create.rb
+++ b/app/graphql/mutations/custom_emoji/create.rb
@@ -12,17 +12,17 @@ module Mutations
field :custom_emoji,
Types::CustomEmojiType,
null: true,
- description: 'The new custom emoji.'
+ description: 'New custom emoji.'
- argument :group_path, GraphQL::ID_TYPE,
+ argument :group_path, GraphQL::Types::ID,
required: true,
description: 'Namespace full path the emoji is associated with.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: true,
description: 'Name of the emoji.'
- argument :url, GraphQL::STRING_TYPE,
+ argument :url, GraphQL::Types::String,
required: true,
as: :file,
description: 'Location of the emoji file.'
diff --git a/app/graphql/mutations/design_management/base.rb b/app/graphql/mutations/design_management/base.rb
index 14d85885793..a6b498c380c 100644
--- a/app/graphql/mutations/design_management/base.rb
+++ b/app/graphql/mutations/design_management/base.rb
@@ -5,13 +5,13 @@ module Mutations
class Base < ::Mutations::BaseMutation
include Mutations::ResolvesIssuable
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project where the issue is to upload designs for."
+ description: "Project where the issue is to upload designs for."
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::Types::ID,
required: true,
- description: "The IID of the issue to modify designs for."
+ description: "IID of the issue to modify designs for."
private
diff --git a/app/graphql/mutations/design_management/delete.rb b/app/graphql/mutations/design_management/delete.rb
index f604542edef..4e9f0aad934 100644
--- a/app/graphql/mutations/design_management/delete.rb
+++ b/app/graphql/mutations/design_management/delete.rb
@@ -7,16 +7,16 @@ module Mutations
graphql_name "DesignManagementDelete"
- argument :filenames, [GraphQL::STRING_TYPE],
+ argument :filenames, [GraphQL::Types::String],
required: true,
- description: "The filenames of the designs to delete.",
+ description: "Filenames of the designs to delete.",
prepare: ->(names, _ctx) do
names.presence || (raise Errors::ArgumentError, 'no filenames')
end
field :version, Types::DesignManagement::VersionType,
null: true, # null on error
- description: 'The new version in which the designs are deleted.'
+ description: 'New version in which the designs are deleted.'
authorize :destroy_design
diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb
index fe280e926d2..1ca03f22880 100644
--- a/app/graphql/mutations/design_management/move.rb
+++ b/app/graphql/mutations/design_management/move.rb
@@ -18,7 +18,7 @@ module Mutations
field :design_collection, Types::DesignManagement::DesignCollectionType,
null: true,
- description: "The current state of the collection."
+ description: "Current state of the collection."
def resolve(**args)
service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(**args))
diff --git a/app/graphql/mutations/design_management/upload.rb b/app/graphql/mutations/design_management/upload.rb
index 2ccf2ef8ff5..f5cb828bae7 100644
--- a/app/graphql/mutations/design_management/upload.rb
+++ b/app/graphql/mutations/design_management/upload.rb
@@ -7,13 +7,13 @@ module Mutations
argument :files, [ApolloUploadServer::Upload],
required: true,
- description: "The files to upload."
+ description: "Files to upload."
authorize :create_design
field :designs, [Types::DesignManagement::DesignType],
null: false,
- description: "The designs that were uploaded by the mutation."
+ description: "Designs that were uploaded by the mutation."
field :skipped_designs, [Types::DesignManagement::DesignType],
null: false,
diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb
index 6639252ec67..2005c9e54e0 100644
--- a/app/graphql/mutations/discussions/toggle_resolve.rb
+++ b/app/graphql/mutations/discussions/toggle_resolve.rb
@@ -10,17 +10,17 @@ module Mutations
argument :id,
Types::GlobalIDType[Discussion],
required: true,
- description: 'The global ID of the discussion.'
+ description: 'Global ID of the discussion.'
argument :resolve,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: 'Will resolve the discussion when true, and unresolve the discussion when false.'
field :discussion,
Types::Notes::DiscussionType,
null: true,
- description: 'The discussion after mutation.'
+ description: 'Discussion after mutation.'
def resolve(id:, resolve:)
discussion = authorized_find_discussion!(id: id)
diff --git a/app/graphql/mutations/echo.rb b/app/graphql/mutations/echo.rb
index 61d39009ba4..a953a9489e7 100644
--- a/app/graphql/mutations/echo.rb
+++ b/app/graphql/mutations/echo.rb
@@ -11,18 +11,18 @@ module Mutations
DOC
argument :errors,
- type: [::GraphQL::STRING_TYPE],
+ type: [::GraphQL::Types::String],
required: false,
description: 'Errors to return to the user.'
argument :messages,
- type: [::GraphQL::STRING_TYPE],
+ type: [::GraphQL::Types::String],
as: :echoes,
required: false,
description: 'Messages to return to the user.'
field :echoes,
- type: [::GraphQL::STRING_TYPE],
+ type: [::GraphQL::Types::String],
null: true,
description: 'Messages returned to the user.'
diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb
index 45dcc8314a0..e4ba08e6dcc 100644
--- a/app/graphql/mutations/environments/canary_ingress/update.rb
+++ b/app/graphql/mutations/environments/canary_ingress/update.rb
@@ -11,12 +11,12 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Environment],
required: true,
- description: 'The global ID of the environment to update.'
+ description: 'Global ID of the environment to update.'
argument :weight,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
required: true,
- description: 'The weight of the Canary Ingress.'
+ description: 'Weight of the Canary Ingress.'
def resolve(id:, **kwargs)
environment = authorized_find!(id: id)
diff --git a/app/graphql/mutations/groups/update.rb b/app/graphql/mutations/groups/update.rb
new file mode 100644
index 00000000000..9c5628a57cd
--- /dev/null
+++ b/app/graphql/mutations/groups/update.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Groups
+ class Update < Mutations::BaseMutation
+ include Mutations::ResolvesGroup
+
+ graphql_name 'GroupUpdate'
+
+ authorize :admin_group
+
+ field :group, Types::GroupType,
+ null: true,
+ description: 'Group after update.'
+
+ argument :full_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the group that will be updated.'
+ argument :shared_runners_setting, Types::Namespace::SharedRunnersSettingEnum,
+ required: true,
+ description: copy_field_description(Types::GroupType, :shared_runners_setting)
+
+ def resolve(full_path:, **args)
+ group = authorized_find!(full_path: full_path)
+
+ unless ::Groups::UpdateService.new(group, current_user, args).execute
+ return { group: nil, errors: group.errors.full_messages }
+ end
+
+ { group: group, errors: [] }
+ end
+
+ private
+
+ def find_object(full_path:)
+ resolve_group(full_path: full_path)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb
index b25987a43f6..2a61e8918a8 100644
--- a/app/graphql/mutations/issues/base.rb
+++ b/app/graphql/mutations/issues/base.rb
@@ -5,18 +5,18 @@ module Mutations
class Base < BaseMutation
include Mutations::ResolvesIssuable
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project the issue to mutate is in."
+ description: "Project the issue to mutate is in."
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
- description: "The IID of the issue to mutate."
+ description: "IID of the issue to mutate."
field :issue,
Types::IssueType,
null: true,
- description: "The issue after mutation."
+ description: "Issue after mutation."
authorize :update_issue
diff --git a/app/graphql/mutations/issues/common_mutation_arguments.rb b/app/graphql/mutations/issues/common_mutation_arguments.rb
index 65768b85d14..36fd94716a6 100644
--- a/app/graphql/mutations/issues/common_mutation_arguments.rb
+++ b/app/graphql/mutations/issues/common_mutation_arguments.rb
@@ -6,7 +6,7 @@ module Mutations
extend ActiveSupport::Concern
included do
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::IssueType, :description)
@@ -14,11 +14,11 @@ module Mutations
required: false,
description: copy_field_description(Types::IssueType, :due_date)
- argument :confidential, GraphQL::BOOLEAN_TYPE,
+ argument :confidential, GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::IssueType, :confidential)
- argument :locked, GraphQL::BOOLEAN_TYPE,
+ argument :locked, GraphQL::Types::Boolean,
as: :discussion_locked,
required: false,
description: copy_field_description(Types::IssueType, :discussion_locked)
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb
index 7c4a851f8aa..32f96f1bfe6 100644
--- a/app/graphql/mutations/issues/create.rb
+++ b/app/graphql/mutations/issues/create.rb
@@ -10,29 +10,29 @@ module Mutations
include CommonMutationArguments
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the issue is associated with.'
- argument :iid, GraphQL::INT_TYPE,
+ argument :iid, GraphQL::Types::Int,
required: false,
- description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify.'
+ description: 'IID (internal ID) of a project issue. Only admins and project owners can modify.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::IssueType, :title)
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
required: false,
- description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null.'
+ description: 'ID of the milestone to assign to the issue. On update milestone will be removed if set to null.'
- argument :labels, [GraphQL::STRING_TYPE],
+ argument :labels, [GraphQL::Types::String],
required: false,
description: copy_field_description(Types::IssueType, :labels)
argument :label_ids, [::Types::GlobalIDType[::Label]],
required: false,
- description: 'The IDs of labels to be added to the issue.'
+ description: 'IDs of labels to be added to the issue.'
argument :created_at, Types::TimeType,
required: false,
@@ -40,20 +40,20 @@ module Mutations
argument :merge_request_to_resolve_discussions_of, ::Types::GlobalIDType[::MergeRequest],
required: false,
- description: 'The IID of a merge request for which to resolve discussions.'
+ description: 'IID of a merge request for which to resolve discussions.'
- argument :discussion_to_resolve, GraphQL::STRING_TYPE,
+ argument :discussion_to_resolve, GraphQL::Types::String,
required: false,
- description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`.'
+ description: 'ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`.'
argument :assignee_ids, [::Types::GlobalIDType[::User]],
required: false,
- description: 'The array of user IDs to assign to the issue.'
+ description: 'Array of user IDs to assign to the issue.'
field :issue,
Types::IssueType,
null: true,
- description: 'The issue after mutation.'
+ description: 'Issue after mutation.'
def ready?(**args)
if args.slice(*mutually_exclusive_label_args).size > 1
diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb
index cb4f0f42b38..fb22a2d891c 100644
--- a/app/graphql/mutations/issues/move.rb
+++ b/app/graphql/mutations/issues/move.rb
@@ -6,9 +6,9 @@ module Mutations
graphql_name 'IssueMove'
argument :target_project_path,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
required: true,
- description: 'The project to move the issue to.'
+ description: 'Project to move the issue to.'
def resolve(project_path:, iid:, target_project_path:)
Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20816')
diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb
index cfee2420ee0..35e629ddc90 100644
--- a/app/graphql/mutations/issues/set_confidential.rb
+++ b/app/graphql/mutations/issues/set_confidential.rb
@@ -8,7 +8,7 @@ module Mutations
graphql_name 'IssueSetConfidential'
argument :confidential,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: 'Whether or not to set the issue as a confidential.'
diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb
index 9cefac96b25..70b76da4fcb 100644
--- a/app/graphql/mutations/issues/set_due_date.rb
+++ b/app/graphql/mutations/issues/set_due_date.rb
@@ -7,17 +7,8 @@ module Mutations
argument :due_date,
Types::TimeType,
- 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
+ required: :nullable,
+ description: 'Desired due date for the issue. Due date is removed if null.'
def resolve(project_path:, iid:, due_date:)
issue = authorized_find!(project_path: project_path, iid: iid)
diff --git a/app/graphql/mutations/issues/set_locked.rb b/app/graphql/mutations/issues/set_locked.rb
index 3a696a64dad..93b31350bbf 100644
--- a/app/graphql/mutations/issues/set_locked.rb
+++ b/app/graphql/mutations/issues/set_locked.rb
@@ -6,7 +6,7 @@ module Mutations
graphql_name 'IssueSetLocked'
argument :locked,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: 'Whether or not to lock discussion on the issue.'
diff --git a/app/graphql/mutations/issues/set_subscription.rb b/app/graphql/mutations/issues/set_subscription.rb
index 55c9049b7cf..1d4fa0eefd8 100644
--- a/app/graphql/mutations/issues/set_subscription.rb
+++ b/app/graphql/mutations/issues/set_subscription.rb
@@ -8,18 +8,18 @@ module Mutations
include ResolvesSubscription
include Mutations::ResolvesIssuable
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project the issue to mutate is in."
+ description: "Project the issue to mutate is in."
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
- description: "The IID of the issue to mutate."
+ description: "IID of the issue to mutate."
field :issue,
Types::IssueType,
null: true,
- description: "The issue after mutation."
+ description: "Issue after mutation."
authorize :update_subscription
diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb
index 1ceed868a6c..6cab1214d24 100644
--- a/app/graphql/mutations/issues/update.rb
+++ b/app/graphql/mutations/issues/update.rb
@@ -7,21 +7,25 @@ module Mutations
include CommonMutationArguments
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::IssueType, :title)
- argument :milestone_id, GraphQL::ID_TYPE, # rubocop: disable Graphql/IDType
+ argument :milestone_id, GraphQL::Types::ID, # rubocop: disable Graphql/IDType
required: false,
- description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null.'
+ description: 'ID of the milestone to assign to the issue. On update milestone will be removed if set to null.'
- argument :add_label_ids, [GraphQL::ID_TYPE],
+ argument :add_label_ids, [GraphQL::Types::ID],
required: false,
- description: 'The IDs of labels to be added to the issue.'
+ description: 'IDs of labels to be added to the issue.'
- argument :remove_label_ids, [GraphQL::ID_TYPE],
+ argument :remove_label_ids, [GraphQL::Types::ID],
required: false,
- description: 'The IDs of labels to be removed from the issue.'
+ description: 'IDs of labels to be removed from the issue.'
+
+ argument :label_ids, [GraphQL::Types::ID],
+ required: false,
+ description: 'IDs of labels to be set. Replaces existing issue labels.'
argument :state_event, Types::IssueStateEventEnum,
description: 'Close or reopen an issue.',
@@ -31,6 +35,8 @@ module Mutations
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
+ args = parse_arguments(args)
+
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
::Issues::UpdateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue)
@@ -39,6 +45,32 @@ module Mutations
errors: errors_on_object(issue)
}
end
+
+ def ready?(label_ids: [], add_label_ids: [], remove_label_ids: [], **args)
+ if label_ids.any? && (add_label_ids.any? || remove_label_ids.any?)
+ raise Gitlab::Graphql::Errors::ArgumentError, 'labelIds is mutually exclusive with any of addLabelIds or removeLabelIds'
+ end
+
+ super
+ end
+
+ private
+
+ def parse_arguments(args)
+ args[:add_label_ids] = parse_label_ids(args[:add_label_ids])
+ args[:remove_label_ids] = parse_label_ids(args[:remove_label_ids])
+ args[:label_ids] = parse_label_ids(args[:label_ids])
+
+ args
+ end
+
+ def parse_label_ids(ids)
+ ids&.map do |gid|
+ GitlabSchema.parse_gid(gid, expected_type: ::Label).model_id
+ rescue Gitlab::Graphql::Errors::ArgumentError
+ gid
+ end
+ end
end
end
end
diff --git a/app/graphql/mutations/jira_import/import_users.rb b/app/graphql/mutations/jira_import/import_users.rb
index af2bb18161f..8d82a058dd0 100644
--- a/app/graphql/mutations/jira_import/import_users.rb
+++ b/app/graphql/mutations/jira_import/import_users.rb
@@ -14,12 +14,12 @@ module Mutations
null: true,
description: 'Users returned from Jira, matched by email and name if possible.'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to import the Jira users into.'
- argument :start_at, GraphQL::INT_TYPE,
+ description: 'Project to import the Jira users into.'
+ argument :start_at, GraphQL::Types::Int,
required: false,
- description: 'The index of the record the import should started at, default 0 (50 records returned).'
+ description: 'Index of the record the import should started at, default 0 (50 records returned).'
def resolve(project_path:, start_at: 0)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/jira_import/start.rb b/app/graphql/mutations/jira_import/start.rb
index e31aaf53a09..143a9558e38 100644
--- a/app/graphql/mutations/jira_import/start.rb
+++ b/app/graphql/mutations/jira_import/start.rb
@@ -12,21 +12,21 @@ module Mutations
field :jira_import,
Types::JiraImportType,
null: true,
- description: 'The Jira import data after mutation.'
+ description: 'Jira import data after mutation.'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project to import the Jira project into.'
- argument :jira_project_key, GraphQL::STRING_TYPE,
+ description: 'Project to import the Jira project into.'
+ argument :jira_project_key, GraphQL::Types::String,
required: true,
description: 'Project key of the importer Jira project.'
- argument :jira_project_name, GraphQL::STRING_TYPE,
+ argument :jira_project_name, GraphQL::Types::String,
required: false,
description: 'Project name of the importer Jira project.'
argument :users_mapping,
[Types::JiraUsersMappingInputType],
required: false,
- description: 'The mapping of Jira to GitLab users.'
+ description: 'Mapping of Jira to GitLab users.'
def resolve(project_path:, jira_project_key:, users_mapping:)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/labels/create.rb b/app/graphql/mutations/labels/create.rb
index 683d0b44586..cb3ba7939ae 100644
--- a/app/graphql/mutations/labels/create.rb
+++ b/app/graphql/mutations/labels/create.rb
@@ -10,17 +10,17 @@ module Mutations
field :label,
Types::LabelType,
null: true,
- description: 'The label after mutation.'
+ description: 'Label after mutation.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: true,
description: 'Title of the label.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description of the label.'
- argument :color, GraphQL::STRING_TYPE,
+ argument :color, GraphQL::Types::String,
required: false,
default_value: Label::DEFAULT_COLOR,
see: {
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index 9994f793a01..d16b2327f2d 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -23,20 +23,20 @@ module Mutations
as: :auto_merge_strategy,
description: 'How to merge this merge request.'
- argument :commit_message, ::GraphQL::STRING_TYPE,
+ argument :commit_message, ::GraphQL::Types::String,
required: false,
description: 'Custom merge commit message.'
- argument :squash_commit_message, ::GraphQL::STRING_TYPE,
+ argument :squash_commit_message, ::GraphQL::Types::String,
required: false,
description: 'Custom squash commit message (if squash is true).'
- argument :sha, ::GraphQL::STRING_TYPE,
+ argument :sha, ::GraphQL::Types::String,
required: true,
- description: 'The HEAD SHA at the time when this merge was requested.'
+ description: 'HEAD SHA at the time when this merge was requested.'
- argument :should_remove_source_branch, ::GraphQL::BOOLEAN_TYPE,
+ argument :should_remove_source_branch, ::GraphQL::Types::Boolean,
required: false,
description: 'Should the source branch be removed.'
- argument :squash, ::GraphQL::BOOLEAN_TYPE,
+ argument :squash, ::GraphQL::Types::Boolean,
required: false,
default_value: false,
description: 'Squash commits on the source branch before merge.'
diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb
index cd919a19ba2..c4bbbf550d9 100644
--- a/app/graphql/mutations/merge_requests/base.rb
+++ b/app/graphql/mutations/merge_requests/base.rb
@@ -5,18 +5,18 @@ module Mutations
class Base < BaseMutation
include Mutations::ResolvesIssuable
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project the merge request to mutate is in."
+ description: "Project the merge request to mutate is in."
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
- description: "The IID of the merge request to mutate."
+ description: "IID of the merge request to mutate."
field :merge_request,
Types::MergeRequestType,
null: true,
- description: "The merge request after mutation."
+ description: "Merge request after mutation."
authorize :update_merge_request
diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb
index 4849c198677..dc1d5a22bc9 100644
--- a/app/graphql/mutations/merge_requests/create.rb
+++ b/app/graphql/mutations/merge_requests/create.rb
@@ -7,34 +7,34 @@ module Mutations
graphql_name 'MergeRequestCreate'
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Project full path the merge request is associated with.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::MergeRequestType, :title)
- argument :source_branch, GraphQL::STRING_TYPE,
+ argument :source_branch, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::MergeRequestType, :source_branch)
- argument :target_branch, GraphQL::STRING_TYPE,
+ argument :target_branch, GraphQL::Types::String,
required: true,
description: copy_field_description(Types::MergeRequestType, :target_branch)
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::MergeRequestType, :description)
- argument :labels, [GraphQL::STRING_TYPE],
+ argument :labels, [GraphQL::Types::String],
required: false,
description: copy_field_description(Types::MergeRequestType, :labels)
field :merge_request,
Types::MergeRequestType,
null: true,
- description: 'The merge request after mutation.'
+ description: 'Merge request after mutation.'
authorize :create_merge_request_from
diff --git a/app/graphql/mutations/merge_requests/reviewer_rereview.rb b/app/graphql/mutations/merge_requests/reviewer_rereview.rb
index d1d5118e271..74f8e282cdd 100644
--- a/app/graphql/mutations/merge_requests/reviewer_rereview.rb
+++ b/app/graphql/mutations/merge_requests/reviewer_rereview.rb
@@ -9,7 +9,7 @@ module Mutations
loads: Types::UserType,
required: true,
description: <<~DESC
- The user ID for the user that has been requested for a new review.
+ User ID for the user that has been requested for a new review.
DESC
def resolve(project_path:, iid:, user:)
diff --git a/app/graphql/mutations/merge_requests/set_draft.rb b/app/graphql/mutations/merge_requests/set_draft.rb
index 80006c6f70e..ab4ca73e5dc 100644
--- a/app/graphql/mutations/merge_requests/set_draft.rb
+++ b/app/graphql/mutations/merge_requests/set_draft.rb
@@ -6,7 +6,7 @@ module Mutations
graphql_name 'MergeRequestSetDraft'
argument :draft,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: <<~DESC
Whether or not to set the merge request as a draft.
diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb
index a77c2731a05..0b40d6c5c5e 100644
--- a/app/graphql/mutations/merge_requests/set_labels.rb
+++ b/app/graphql/mutations/merge_requests/set_labels.rb
@@ -9,7 +9,7 @@ module Mutations
[::Types::GlobalIDType[Label]],
required: true,
description: <<~DESC
- The Label IDs to set. Replaces existing labels by default.
+ Label IDs to set. Replaces existing labels by default.
DESC
argument :operation_mode,
diff --git a/app/graphql/mutations/merge_requests/set_locked.rb b/app/graphql/mutations/merge_requests/set_locked.rb
index e9e607551a6..8f7b39be777 100644
--- a/app/graphql/mutations/merge_requests/set_locked.rb
+++ b/app/graphql/mutations/merge_requests/set_locked.rb
@@ -6,7 +6,7 @@ module Mutations
graphql_name 'MergeRequestSetLocked'
argument :locked,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: <<~DESC
Whether or not to lock the merge request.
diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb
index ed5139c4af9..bf40c12aec5 100644
--- a/app/graphql/mutations/merge_requests/set_milestone.rb
+++ b/app/graphql/mutations/merge_requests/set_milestone.rb
@@ -10,7 +10,7 @@ module Mutations
required: false,
loads: Types::MilestoneType,
description: <<~DESC
- The milestone to assign to the merge request.
+ Milestone to assign to the merge request.
DESC
def resolve(project_path:, iid:, milestone: nil)
diff --git a/app/graphql/mutations/merge_requests/set_subscription.rb b/app/graphql/mutations/merge_requests/set_subscription.rb
index 981daa81c28..9d6a54a84fd 100644
--- a/app/graphql/mutations/merge_requests/set_subscription.rb
+++ b/app/graphql/mutations/merge_requests/set_subscription.rb
@@ -8,18 +8,18 @@ module Mutations
include ResolvesSubscription
include Mutations::ResolvesIssuable
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: "The project the merge request to mutate is in."
+ description: "Project the merge request to mutate is in."
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
- description: "The IID of the merge request to mutate."
+ description: "IID of the merge request to mutate."
field :merge_request,
Types::MergeRequestType,
null: true,
- description: "The merge request after mutation."
+ description: "Merge request after mutation."
authorize :update_subscription
diff --git a/app/graphql/mutations/merge_requests/set_wip.rb b/app/graphql/mutations/merge_requests/set_wip.rb
index 6f52b240840..9b6b67d4b4f 100644
--- a/app/graphql/mutations/merge_requests/set_wip.rb
+++ b/app/graphql/mutations/merge_requests/set_wip.rb
@@ -6,7 +6,7 @@ module Mutations
graphql_name 'MergeRequestSetWip'
argument :wip,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: true,
description: <<~DESC
Whether or not to set the merge request as a draft.
diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb
index 246e468c34c..0f4923e15a1 100644
--- a/app/graphql/mutations/merge_requests/update.rb
+++ b/app/graphql/mutations/merge_requests/update.rb
@@ -7,22 +7,22 @@ module Mutations
description 'Update attributes of a merge request'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::MergeRequestType, :title)
- argument :target_branch, GraphQL::STRING_TYPE,
+ argument :target_branch, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::MergeRequestType, :target_branch)
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::MergeRequestType, :description)
argument :state, ::Types::MergeRequestStateEventEnum,
required: false,
as: :state_event,
- description: 'The action to perform to change the state.'
+ description: 'Action to perform to change the state.'
def resolve(project_path:, iid:, **args)
merge_request = authorized_find!(project_path: project_path, iid: iid)
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
index 85937809eb8..2eb48c9029d 100644
--- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb
+++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
@@ -15,17 +15,17 @@ module Mutations
field :annotation,
Types::Metrics::Dashboards::AnnotationType,
null: true,
- description: 'The created annotation.'
+ description: 'Created annotation.'
argument :environment_id,
::Types::GlobalIDType[::Environment],
required: false,
- description: 'The global ID of the environment to add an annotation to.'
+ description: 'Global ID of the environment to add an annotation to.'
argument :cluster_id,
::Types::GlobalIDType[::Clusters::Cluster],
required: false,
- description: 'The global ID of the cluster to add an annotation to.'
+ description: 'Global ID of the cluster to add an annotation to.'
argument :starting_at, Types::TimeType,
required: true,
@@ -36,14 +36,14 @@ module Mutations
description: 'Timestamp indicating ending moment to which the annotation relates.'
argument :dashboard_path,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
- description: 'The path to a file defining the dashboard on which the annotation should be added.'
+ description: 'Path to a file defining the dashboard on which the annotation should be added.'
argument :description,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
- description: 'The description of the annotation.'
+ description: 'Description of the annotation.'
AnnotationSource = Struct.new(:object, keyword_init: true) do
def type_keys
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index 75c80cfbd3e..400169d6b64 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -11,12 +11,12 @@ module Mutations
authorize :create_package_settings
argument :namespace_path,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
required: true,
- description: 'The namespace path where the namespace package setting is located.'
+ description: 'Namespace path where the namespace package setting is located.'
argument :maven_duplicates_allowed,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicates_allowed)
@@ -26,7 +26,7 @@ module Mutations
description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicate_exception_regex)
argument :generic_duplicates_allowed,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :generic_duplicates_allowed)
@@ -38,7 +38,7 @@ module Mutations
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,
- description: 'The namespace package setting after mutation.'
+ description: 'Namespace package setting after mutation.'
def resolve(namespace_path:, **args)
namespace = authorized_find!(namespace_path: namespace_path)
diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb
index ff401167ba1..d6c8121eee7 100644
--- a/app/graphql/mutations/notes/base.rb
+++ b/app/graphql/mutations/notes/base.rb
@@ -6,7 +6,7 @@ module Mutations
field :note,
Types::Notes::NoteType,
null: true,
- description: 'The note after mutation.'
+ description: 'Note after mutation.'
private
diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb
index a157a5abdf2..e13a51c6862 100644
--- a/app/graphql/mutations/notes/create/base.rb
+++ b/app/graphql/mutations/notes/create/base.rb
@@ -11,17 +11,17 @@ module Mutations
argument :noteable_id,
::Types::GlobalIDType[::Noteable],
required: true,
- description: 'The global ID of the resource to add a note to.'
+ description: 'Global ID of the resource to add a note to.'
argument :body,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
description: copy_field_description(Types::Notes::NoteType, :body)
argument :confidential,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
- description: 'The confidentiality flag of a note. Default is false.'
+ description: 'Confidentiality flag of a note. Default is false.'
def resolve(args)
noteable = authorized_find!(id: args[:noteable_id])
diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb
index 7af93521e0d..5a5d62a8c20 100644
--- a/app/graphql/mutations/notes/create/note.rb
+++ b/app/graphql/mutations/notes/create/note.rb
@@ -9,7 +9,7 @@ module Mutations
argument :discussion_id,
::Types::GlobalIDType[::Discussion],
required: false,
- description: 'The global ID of the discussion this note is in reply to.'
+ description: 'Global ID of the discussion this note is in reply to.'
private
diff --git a/app/graphql/mutations/notes/destroy.rb b/app/graphql/mutations/notes/destroy.rb
index 21b2cd15a7e..43a69021aaf 100644
--- a/app/graphql/mutations/notes/destroy.rb
+++ b/app/graphql/mutations/notes/destroy.rb
@@ -10,7 +10,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Note],
required: true,
- description: 'The global ID of the note to destroy.'
+ description: 'Global ID of the note to destroy.'
def resolve(id:)
note = authorized_find!(id: id)
diff --git a/app/graphql/mutations/notes/reposition_image_diff_note.rb b/app/graphql/mutations/notes/reposition_image_diff_note.rb
index 31d3b7c9bb0..ec68f077c84 100644
--- a/app/graphql/mutations/notes/reposition_image_diff_note.rb
+++ b/app/graphql/mutations/notes/reposition_image_diff_note.rb
@@ -16,7 +16,7 @@ module Mutations
loads: Types::Notes::NoteType,
as: :note,
required: true,
- description: 'The global ID of the DiffNote to update.'
+ description: 'Global ID of the DiffNote to update.'
argument :position,
Types::Notes::UpdateDiffImagePositionInputType,
diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb
index 571001981a4..2dfa7b815a1 100644
--- a/app/graphql/mutations/notes/update/base.rb
+++ b/app/graphql/mutations/notes/update/base.rb
@@ -17,7 +17,7 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Note],
required: true,
- description: 'The global ID of the note to update.'
+ description: 'Global ID of the note to update.'
def resolve(args)
note = authorized_find!(id: args[:id])
diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb
index 6160ee03f4e..284c0f1bb20 100644
--- a/app/graphql/mutations/notes/update/image_diff_note.rb
+++ b/app/graphql/mutations/notes/update/image_diff_note.rb
@@ -11,7 +11,7 @@ module Mutations
DESC
argument :body,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: copy_field_description(Types::Notes::NoteType, :body)
diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb
index 11d8c6e2cb9..c7ee0148f94 100644
--- a/app/graphql/mutations/notes/update/note.rb
+++ b/app/graphql/mutations/notes/update/note.rb
@@ -8,14 +8,14 @@ module Mutations
description "Updates a Note.\n#{QUICK_ACTION_ONLY_WARNING}"
argument :body,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: copy_field_description(Types::Notes::NoteType, :body)
argument :confidential,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
- description: 'The confidentiality flag of a note. Default is false.'
+ description: 'Confidentiality flag of a note. Default is false.'
private
diff --git a/app/graphql/mutations/packages/destroy_file.rb b/app/graphql/mutations/packages/destroy_file.rb
new file mode 100644
index 00000000000..35a486666d5
--- /dev/null
+++ b/app/graphql/mutations/packages/destroy_file.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ class DestroyFile < ::Mutations::BaseMutation
+ graphql_name 'DestroyPackageFile'
+
+ authorize :destroy_package
+
+ argument :id,
+ ::Types::GlobalIDType[::Packages::PackageFile],
+ required: true,
+ description: 'ID of the Package file.'
+
+ def resolve(id:)
+ package_file = authorized_find!(id: id)
+
+ if package_file.destroy
+ return { errors: [] }
+ end
+
+ { errors: package_file.errors.full_messages }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ::Types::GlobalIDType[::Packages::PackageFile].coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/release_asset_links/create.rb b/app/graphql/mutations/release_asset_links/create.rb
index ff9d98d2c0f..db486640507 100644
--- a/app/graphql/mutations/release_asset_links/create.rb
+++ b/app/graphql/mutations/release_asset_links/create.rb
@@ -11,18 +11,18 @@ module Mutations
include Types::ReleaseAssetLinkSharedInputArguments
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project the asset link is associated with.'
- argument :tag_name, GraphQL::STRING_TYPE,
+ argument :tag_name, GraphQL::Types::String,
required: true, as: :tag,
description: "Name of the associated release's tag."
field :link,
Types::ReleaseAssetLinkType,
null: true,
- description: 'The asset link after mutation.'
+ description: 'Asset link after mutation.'
def resolve(project_path:, tag:, **link_attrs)
project = authorized_find!(project_path)
diff --git a/app/graphql/mutations/release_asset_links/delete.rb b/app/graphql/mutations/release_asset_links/delete.rb
index dd450f36cdd..d8f0946670b 100644
--- a/app/graphql/mutations/release_asset_links/delete.rb
+++ b/app/graphql/mutations/release_asset_links/delete.rb
@@ -16,7 +16,7 @@ module Mutations
field :link,
Types::ReleaseAssetLinkType,
null: true,
- description: 'The deleted release asset link.'
+ description: 'Deleted release asset link.'
def resolve(id:)
link = authorized_find!(id)
diff --git a/app/graphql/mutations/release_asset_links/update.rb b/app/graphql/mutations/release_asset_links/update.rb
index 1d9460bde78..18d92cd82ae 100644
--- a/app/graphql/mutations/release_asset_links/update.rb
+++ b/app/graphql/mutations/release_asset_links/update.rb
@@ -13,26 +13,26 @@ module Mutations
required: true,
description: 'ID of the release asset link to update.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
description: 'Name of the asset link.'
- argument :url, GraphQL::STRING_TYPE,
+ argument :url, GraphQL::Types::String,
required: false,
description: 'URL of the asset link.'
- argument :direct_asset_path, GraphQL::STRING_TYPE,
+ argument :direct_asset_path, GraphQL::Types::String,
required: false, as: :filepath,
description: 'Relative path for a direct asset link.'
argument :link_type, Types::ReleaseAssetLinkTypeEnum,
required: false,
- description: 'The type of the asset link.'
+ description: 'Type of the asset link.'
field :link,
Types::ReleaseAssetLinkType,
null: true,
- description: 'The asset link after mutation.'
+ description: 'Asset link after mutation.'
def ready?(**args)
if args.key?(:link_type) && args[:link_type].nil?
diff --git a/app/graphql/mutations/releases/base.rb b/app/graphql/mutations/releases/base.rb
index 610e9cd9cde..a161dd73bdd 100644
--- a/app/graphql/mutations/releases/base.rb
+++ b/app/graphql/mutations/releases/base.rb
@@ -5,7 +5,7 @@ module Mutations
class Base < BaseMutation
include FindsProject
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project the release is associated with.'
end
diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb
index 914c1302094..037ade2589c 100644
--- a/app/graphql/mutations/releases/create.rb
+++ b/app/graphql/mutations/releases/create.rb
@@ -8,31 +8,31 @@ module Mutations
field :release,
Types::ReleaseType,
null: true,
- description: 'The release after mutation.'
+ description: 'Release after mutation.'
- argument :tag_name, GraphQL::STRING_TYPE,
+ argument :tag_name, GraphQL::Types::String,
required: true, as: :tag,
description: 'Name of the tag to associate with the release.'
- argument :ref, GraphQL::STRING_TYPE,
+ argument :ref, GraphQL::Types::String,
required: false,
- description: 'The commit SHA or branch name to use if creating a new tag.'
+ description: 'Commit SHA or branch name to use if creating a new tag.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
description: 'Name of the release.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description (also known as "release notes") of the release.'
argument :released_at, Types::TimeType,
required: false,
- description: 'The date when the release will be/was ready. Defaults to the current time.'
+ description: 'Date and time for the release. Defaults to the current date and time.'
- argument :milestones, [GraphQL::STRING_TYPE],
+ argument :milestones, [GraphQL::Types::String],
required: false,
- description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
+ description: 'Title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
argument :assets, Types::ReleaseAssetsInputType,
required: false,
diff --git a/app/graphql/mutations/releases/delete.rb b/app/graphql/mutations/releases/delete.rb
index 020c9133b58..70f12577054 100644
--- a/app/graphql/mutations/releases/delete.rb
+++ b/app/graphql/mutations/releases/delete.rb
@@ -8,9 +8,9 @@ module Mutations
field :release,
Types::ReleaseType,
null: true,
- description: 'The deleted release.'
+ description: 'Deleted release.'
- argument :tag_name, GraphQL::STRING_TYPE,
+ argument :tag_name, GraphQL::Types::String,
required: true, as: :tag,
description: 'Name of the tag associated with the release to delete.'
diff --git a/app/graphql/mutations/releases/update.rb b/app/graphql/mutations/releases/update.rb
index 35f2a7b3d4b..549600f7653 100644
--- a/app/graphql/mutations/releases/update.rb
+++ b/app/graphql/mutations/releases/update.rb
@@ -8,27 +8,27 @@ module Mutations
field :release,
Types::ReleaseType,
null: true,
- description: 'The release after mutation.'
+ description: 'Release after mutation.'
- argument :tag_name, GraphQL::STRING_TYPE,
+ argument :tag_name, GraphQL::Types::String,
required: true, as: :tag,
description: 'Name of the tag associated with the release.'
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
description: 'Name of the release.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description (release notes) of the release.'
argument :released_at, Types::TimeType,
required: false,
- description: 'The release date.'
+ description: 'Release date.'
- argument :milestones, [GraphQL::STRING_TYPE],
+ argument :milestones, [GraphQL::Types::String],
required: false,
- description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
+ description: 'Title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
authorize :update_release
diff --git a/app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb b/app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb
index 090a9a4e0ef..e5bb5b6d573 100644
--- a/app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb
+++ b/app/graphql/mutations/security/ci_configuration/base_security_analyzer.rb
@@ -6,14 +6,14 @@ module Mutations
class BaseSecurityAnalyzer < BaseMutation
include FindsProject
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project.'
- field :success_path, GraphQL::STRING_TYPE, null: true,
+ field :success_path, GraphQL::Types::String, null: true,
description: 'Redirect path to use when the response is successful.'
- field :branch, GraphQL::STRING_TYPE, null: true,
+ field :branch, GraphQL::Types::String, null: true,
description: 'Branch that has the new/modified `.gitlab-ci.yml` file.'
authorize :push_code
diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb
index 5196bc5c7ed..acaa7b80843 100644
--- a/app/graphql/mutations/snippets/base.rb
+++ b/app/graphql/mutations/snippets/base.rb
@@ -6,7 +6,7 @@ module Mutations
field :snippet,
Types::SnippetType,
null: true,
- description: 'The snippet after mutation.'
+ description: 'Snippet after mutation.'
private
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index 765163e73a1..c01b0e4a01b 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -14,27 +14,27 @@ module Mutations
field :snippet,
Types::SnippetType,
null: true,
- description: 'The snippet after mutation.'
+ description: 'Snippet after mutation.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: true,
description: 'Title of the snippet.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description of the snippet.'
argument :visibility_level, Types::VisibilityLevelsEnum,
- description: 'The visibility level of the snippet.',
+ description: 'Visibility level of the snippet.',
required: true
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: false,
- description: 'The project full path the snippet is associated with.'
+ description: 'Full path of the project the snippet is associated with.'
- argument :uploaded_files, [GraphQL::STRING_TYPE],
+ argument :uploaded_files, [GraphQL::Types::String],
required: false,
- description: 'The paths to files uploaded in the snippet description.'
+ description: 'Paths to files uploaded in the snippet description.'
argument :blob_actions, [Types::Snippets::BlobActionInputType],
description: 'Actions to perform over the snippet repository and blobs.',
diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb
index 9b00f62e2f9..29358df3de5 100644
--- a/app/graphql/mutations/snippets/destroy.rb
+++ b/app/graphql/mutations/snippets/destroy.rb
@@ -9,7 +9,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
- description: 'The global ID of the snippet to destroy.'
+ description: 'Global ID of the snippet to destroy.'
def resolve(id:)
snippet = authorized_find!(id: id)
diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb
index d6e3e131b81..e4626cff3e5 100644
--- a/app/graphql/mutations/snippets/mark_as_spam.rb
+++ b/app/graphql/mutations/snippets/mark_as_spam.rb
@@ -7,7 +7,7 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
- description: 'The global ID of the snippet to update.'
+ description: 'Global ID of the snippet to update.'
def resolve(id:)
snippet = authorized_find!(id: id)
@@ -23,7 +23,7 @@ module Mutations
private
def mark_as_spam(snippet)
- Spam::MarkAsSpamService.new(target: snippet).execute
+ Spam::AkismetMarkAsSpamService.new(target: snippet).execute
end
def authorized_resource?(snippet)
diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb
index 792c631e5ca..9ecaa8d4bf2 100644
--- a/app/graphql/mutations/snippets/update.rb
+++ b/app/graphql/mutations/snippets/update.rb
@@ -11,18 +11,18 @@ module Mutations
argument :id, ::Types::GlobalIDType[::Snippet],
required: true,
- description: 'The global ID of the snippet to update.'
+ description: 'Global ID of the snippet to update.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: false,
description: 'Title of the snippet.'
- argument :description, GraphQL::STRING_TYPE,
+ argument :description, GraphQL::Types::String,
required: false,
description: 'Description of the snippet.'
argument :visibility_level, Types::VisibilityLevelsEnum,
- description: 'The visibility level of the snippet.',
+ description: 'Visibility level of the snippet.',
required: false
argument :blob_actions, [Types::Snippets::BlobActionInputType],
diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb
index b6250b0228c..ccc3d0b61e4 100644
--- a/app/graphql/mutations/todos/create.rb
+++ b/app/graphql/mutations/todos/create.rb
@@ -10,11 +10,11 @@ module Mutations
argument :target_id,
Types::GlobalIDType[Todoable],
required: true,
- description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported."
+ description: "Global ID of the to-do item's parent. Issues, merge requests, designs, and epics are supported."
field :todo, Types::TodoType,
null: true,
- description: 'The to-do item created.'
+ description: 'To-do item created.'
def resolve(target_id:)
id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id)
diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb
index a78cc91da68..4fecba55242 100644
--- a/app/graphql/mutations/todos/mark_done.rb
+++ b/app/graphql/mutations/todos/mark_done.rb
@@ -10,11 +10,11 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Todo],
required: true,
- description: 'The global ID of the to-do item to mark as done.'
+ description: 'Global ID of the to-do item to mark as done.'
field :todo, Types::TodoType,
null: false,
- description: 'The requested to-do item.'
+ description: 'Requested to-do item.'
def resolve(id:)
todo = authorized_find!(id: id)
diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb
index 70c33c439c4..def24cb71bc 100644
--- a/app/graphql/mutations/todos/restore.rb
+++ b/app/graphql/mutations/todos/restore.rb
@@ -10,11 +10,11 @@ module Mutations
argument :id,
::Types::GlobalIDType[::Todo],
required: true,
- description: 'The global ID of the to-do item to restore.'
+ description: 'Global ID of the to-do item to restore.'
field :todo, Types::TodoType,
null: false,
- description: 'The requested to-do item.'
+ description: 'Requested to-do item.'
def resolve(id:)
todo = authorized_find!(id: id)
diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb
index b09c59a3435..3453645000b 100644
--- a/app/graphql/mutations/todos/restore_many.rb
+++ b/app/graphql/mutations/todos/restore_many.rb
@@ -10,7 +10,7 @@ module Mutations
argument :ids,
[::Types::GlobalIDType[::Todo]],
required: true,
- description: 'The global IDs of the to-do items to restore (a maximum of 50 is supported at once).'
+ description: 'Global IDs of the to-do items to restore (a maximum of 50 is supported at once).'
field :todos, [::Types::TodoType],
null: false,
diff --git a/app/graphql/mutations/user_callouts/create.rb b/app/graphql/mutations/user_callouts/create.rb
index 0d3dcacda41..ff6e5cd28dd 100644
--- a/app/graphql/mutations/user_callouts/create.rb
+++ b/app/graphql/mutations/user_callouts/create.rb
@@ -6,13 +6,13 @@ module Mutations
graphql_name 'UserCalloutCreate'
argument :feature_name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
- description: "The feature name you want to dismiss the callout for."
+ description: "Feature name you want to dismiss the callout for."
field :user_callout, Types::UserCalloutType,
null: false,
- description: 'The user callout dismissed.'
+ description: 'User callout dismissed.'
def resolve(feature_name:)
callout = Users::DismissUserCalloutService.new(
diff --git a/app/graphql/queries/design_management/get_design_list.query.graphql b/app/graphql/queries/design_management/get_design_list.query.graphql
index ade03d99797..01503a9572f 100644
--- a/app/graphql/queries/design_management/get_design_list.query.graphql
+++ b/app/graphql/queries/design_management/get_design_list.query.graphql
@@ -32,6 +32,13 @@ query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
__typename
id
sha
+ createdAt
+ author {
+ __typename
+ id
+ name
+ avatarUrl
+ }
}
}
}
diff --git a/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
index a5f81149d4e..b5a19d38b9c 100644
--- a/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
+++ b/app/graphql/resolvers/admin/analytics/usage_trends/measurements_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
argument :identifier, Types::Admin::Analytics::UsageTrends::MeasurementIdentifierEnum,
required: true,
- description: 'The type of measurement/statistics to retrieve.'
+ description: 'Type of measurement or statistics to retrieve.'
argument :recorded_after, Types::TimeType,
required: false,
diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb
index 62744e719da..899b90a94dd 100644
--- a/app/graphql/resolvers/alert_management/alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management/alert_resolver.rb
@@ -5,7 +5,7 @@ module Resolvers
class AlertResolver < BaseResolver
include LooksAhead
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: false,
description: 'IID of the alert. For example, "1".'
@@ -23,11 +23,11 @@ module Resolvers
required: true,
default_value: 'operations'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
- argument :assignee_username, GraphQL::STRING_TYPE,
+ argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.'
diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
index 410f72cff84..568d648a95e 100644
--- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
+++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
@@ -5,11 +5,11 @@ module Resolvers
class AlertStatusCountsResolver < BaseResolver
type Types::AlertManagement::AlertStatusCountsType, null: true
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
description: 'Search query for title, description, service, or monitoring_tool.',
required: false
- argument :assignee_username, GraphQL::STRING_TYPE,
+ argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.'
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
index d006769bd4b..d0eb2deaf48 100644
--- a/app/graphql/resolvers/blobs_resolver.rb
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -10,13 +10,13 @@ module Resolvers
alias_method :repository, :object
- argument :paths, [GraphQL::STRING_TYPE],
+ argument :paths, [GraphQL::Types::String],
required: true,
description: 'Array of desired blob paths.'
- argument :ref, GraphQL::STRING_TYPE,
+ argument :ref, GraphQL::Types::String,
required: false,
default_value: nil,
- description: 'The commit ref to get the blobs from. Default value is HEAD.'
+ description: 'Commit ref to get the blobs from. Default value is HEAD.'
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
# number of paths being fetched, so apply a scaling limit to that.
diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb
index 85362ab1422..24df67b7b6e 100644
--- a/app/graphql/resolvers/board_resolver.rb
+++ b/app/graphql/resolvers/board_resolver.rb
@@ -8,7 +8,7 @@ module Resolvers
argument :id, ::Types::GlobalIDType[::Board],
required: true,
- description: 'The board\'s ID.'
+ description: 'ID of the board.'
def resolve(id: nil)
return unless parent
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
index f2e33251b9c..2d74392a84c 100644
--- a/app/graphql/resolvers/ci/config_resolver.rb
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -14,19 +14,19 @@ module Resolvers
authorize :read_pipeline
- argument :project_path, GraphQL::ID_TYPE,
+ argument :project_path, GraphQL::Types::ID,
required: true,
- description: 'The project of the CI config.'
+ description: 'Project of the CI config.'
- argument :sha, GraphQL::STRING_TYPE,
+ argument :sha, GraphQL::Types::String,
required: false,
description: "Sha for the pipeline."
- argument :content, GraphQL::STRING_TYPE,
+ argument :content, GraphQL::Types::String,
required: true,
description: "Contents of `.gitlab-ci.yml`."
- argument :dry_run, GraphQL::BOOLEAN_TYPE,
+ argument :dry_run, GraphQL::Types::Boolean,
required: false,
description: 'Run pipeline creation simulation, or only do static check.'
diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb
index 9166999b400..6b21f5c3ab4 100644
--- a/app/graphql/resolvers/ci/runner_setup_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb
@@ -9,12 +9,12 @@ module Resolvers
description 'Runner setup instructions.'
argument :platform,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
required: true,
description: 'Platform to generate the instructions for.'
argument :architecture,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
required: true,
description: 'Architecture to generate the instructions for.'
diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb
index 5074a248e18..1957c4ec058 100644
--- a/app/graphql/resolvers/ci/runners_resolver.rb
+++ b/app/graphql/resolvers/ci/runners_resolver.rb
@@ -15,11 +15,11 @@ module Resolvers
required: false,
description: 'Filter runners by type.'
- argument :tag_list, [GraphQL::STRING_TYPE],
+ argument :tag_list, [GraphQL::Types::String],
required: false,
description: 'Filter by tags associated with the runner (comma-separated or array).'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Filter by full token or partial text in description field.'
diff --git a/app/graphql/resolvers/ci/template_resolver.rb b/app/graphql/resolvers/ci/template_resolver.rb
index 7f5a1a486d7..17f2668df11 100644
--- a/app/graphql/resolvers/ci/template_resolver.rb
+++ b/app/graphql/resolvers/ci/template_resolver.rb
@@ -5,7 +5,7 @@ module Resolvers
class TemplateResolver < BaseResolver
type Types::Ci::TemplateType, null: true
- argument :name, GraphQL::STRING_TYPE, required: true,
+ argument :name, GraphQL::Types::String, required: true,
description: 'Name of the CI/CD template to search for. Template must be formatted as `Name.gitlab-ci.yml`.'
alias_method :project, :object
diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb
index 90cc30b1281..5d61d9e986b 100644
--- a/app/graphql/resolvers/ci/test_suite_resolver.rb
+++ b/app/graphql/resolvers/ci/test_suite_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
alias_method :pipeline, :object
- argument :build_ids, [GraphQL::ID_TYPE],
+ argument :build_ids, [GraphQL::Types::ID],
required: true,
description: 'IDs of the builds used to run the test suite.'
diff --git a/app/graphql/resolvers/concerns/group_issuable_resolver.rb b/app/graphql/resolvers/concerns/group_issuable_resolver.rb
index 49a79683e9f..542ff5374ff 100644
--- a/app/graphql/resolvers/concerns/group_issuable_resolver.rb
+++ b/app/graphql/resolvers/concerns/group_issuable_resolver.rb
@@ -5,7 +5,7 @@ module GroupIssuableResolver
class_methods do
def include_subgroups(name_of_things)
- argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
+ argument :include_subgroups, GraphQL::Types::Boolean,
required: false,
default_value: false,
description: "Include #{name_of_things} belonging to subgroups"
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index c24f4dedc0e..8d77c0f3a8d 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -6,29 +6,29 @@ module IssueResolverArguments
prepended do
include LooksAhead
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: false,
description: 'IID of the issue. For example, "1".'
- argument :iids, [GraphQL::STRING_TYPE],
+ argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues. For example, `["1", "2"]`.'
- argument :label_name, [GraphQL::STRING_TYPE, null: true],
+ argument :label_name, [GraphQL::Types::String, null: true],
required: false,
description: 'Labels applied to this issue.'
- argument :milestone_title, [GraphQL::STRING_TYPE, null: true],
+ argument :milestone_title, [GraphQL::Types::String, null: true],
required: false,
description: 'Milestone applied to this issue.'
- argument :author_username, GraphQL::STRING_TYPE,
+ argument :author_username, GraphQL::Types::String,
required: false,
description: 'Username of the author of the issue.'
- argument :assignee_username, GraphQL::STRING_TYPE,
+ argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of a user assigned to the issue.',
deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
- argument :assignee_usernames, [GraphQL::STRING_TYPE],
+ argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users assigned to the issue.'
- argument :assignee_id, GraphQL::STRING_TYPE,
+ argument :assignee_id, GraphQL::Types::String,
required: false,
description: 'ID of a user assigned to the issues, "none" and "any" values are supported.'
argument :created_before, Types::TimeType,
@@ -49,13 +49,16 @@ module IssueResolverArguments
argument :closed_after, Types::TimeType,
required: false,
description: 'Issues closed after this date.'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
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
+ argument :milestone_wildcard_id, ::Types::MilestoneWildcardIdEnum,
+ required: false,
+ description: 'Filter issues by milestone ID wildcard.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
@@ -82,10 +85,9 @@ module IssueResolverArguments
end
def ready?(**args)
- if args.slice(*mutually_exclusive_assignee_username_args).compact.size > 1
- arg_str = mutually_exclusive_assignee_username_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
- raise Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
- end
+ params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
+ params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
+ params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
super
end
@@ -106,6 +108,17 @@ module IssueResolverArguments
args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
end
+ def params_not_mutually_exclusive(args, mutually_exclusive_args)
+ if args.slice(*mutually_exclusive_args).compact.size > 1
+ arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
+ raise ::Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
+ end
+ end
+
+ def mutually_exclusive_milestone_args
+ [:milestone_title, :milestone_wildcard_id]
+ end
+
def mutually_exclusive_assignee_username_args
[:assignee_usernames, :assignee_username]
end
diff --git a/app/graphql/resolvers/concerns/resolves_ids.rb b/app/graphql/resolvers/concerns/resolves_ids.rb
new file mode 100644
index 00000000000..8bf2a6b2ac9
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_ids.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ResolvesIds
+ extend ActiveSupport::Concern
+
+ def resolve_ids(ids, type)
+ Array.wrap(ids).map do |id|
+ next unless id.present?
+
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = type.coerce_isolated_input(id)
+ id.model_id
+ end.compact
+ end
+end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index bbf33c0b5eb..77f2105db7c 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -10,11 +10,11 @@ module ResolvesPipelines
required: false,
description: "Filter pipelines by their status."
argument :ref,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: "Filter pipelines by the ref they are run for."
argument :sha,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: "Filter pipelines by the sha of the commit they are run for."
end
diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb
index 8de85c074ec..46d78a6a8c8 100644
--- a/app/graphql/resolvers/concerns/resolves_snippets.rb
+++ b/app/graphql/resolvers/concerns/resolves_snippets.rb
@@ -2,6 +2,7 @@
module ResolvesSnippets
extend ActiveSupport::Concern
+ include ResolvesIds
included do
type Types::SnippetType.connection_type, null: true
@@ -12,7 +13,7 @@ module ResolvesSnippets
argument :visibility, Types::Snippets::VisibilityScopesEnum,
required: false,
- description: 'The visibility of the snippet.'
+ description: 'Visibility of the snippet.'
end
def resolve(**args)
@@ -27,22 +28,11 @@ module ResolvesSnippets
def snippet_finder_params(args)
{
- ids: resolve_ids(args[:ids]),
+ ids: resolve_ids(args[:ids], ::Types::GlobalIDType[::Snippet]),
scope: args[:visibility]
}.merge(options_by_type(args[:type]))
end
- def resolve_ids(ids, type = ::Types::GlobalIDType[::Snippet])
- Array.wrap(ids).map do |id|
- next unless id.present?
-
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = type.coerce_isolated_input(id)
- id.model_id
- end.compact
- end
-
def options_by_type(type)
case type
when 'personal'
diff --git a/app/graphql/resolvers/container_repositories_resolver.rb b/app/graphql/resolvers/container_repositories_resolver.rb
index 17af2a2f070..1e06d4e9094 100644
--- a/app/graphql/resolvers/container_repositories_resolver.rb
+++ b/app/graphql/resolvers/container_repositories_resolver.rb
@@ -6,7 +6,7 @@ module Resolvers
type Types::ContainerRepositoryType, null: true
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
description: 'Filter the container repositories by their name.'
diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
index 533692e2b12..c87670bc24c 100644
--- a/app/graphql/resolvers/design_management/design_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
argument :id, ::Types::GlobalIDType[::DesignManagement::DesignAtVersion],
required: true,
- description: 'The Global ID of the design at this version.'
+ description: 'Global ID of the design at this version.'
def resolve(id:)
authorized_find!(id: id)
diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb
index e640f57f04a..d9e5203ef0e 100644
--- a/app/graphql/resolvers/design_management/design_resolver.rb
+++ b/app/graphql/resolvers/design_management/design_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
required: false,
description: 'Find a design by its ID.'
- argument :filename, GraphQL::STRING_TYPE,
+ argument :filename, GraphQL::Types::String,
required: false,
description: 'Find a design by its filename.'
diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb
index c45e4d01f6e..dec778fac80 100644
--- a/app/graphql/resolvers/design_management/designs_resolver.rb
+++ b/app/graphql/resolvers/design_management/designs_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
argument :ids, [DesignID],
required: false,
description: 'Filters designs by their ID.'
- argument :filenames, [GraphQL::STRING_TYPE],
+ argument :filenames, [GraphQL::Types::String],
required: false,
description: 'Filters designs by their filename.'
argument :at_version, VersionID,
diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
index fea74cbfb0b..d879c1434dc 100644
--- a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
@@ -19,13 +19,13 @@ module Resolvers
argument :id, DesignAtVersionID,
required: false,
as: :design_at_version_id,
- description: 'The ID of the DesignAtVersion.'
+ description: 'ID of the DesignAtVersion.'
argument :design_id, DesignID,
required: false,
- description: 'The ID of a specific design.'
- argument :filename, GraphQL::STRING_TYPE,
+ description: 'ID of a specific design.'
+ argument :filename, GraphQL::Types::String,
required: false,
- description: 'The filename of a specific design.'
+ description: 'Filename of a specific design.'
def self.single
self
diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
index 930b1b60d0e..254f1efa0a5 100644
--- a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
@@ -17,7 +17,7 @@ module Resolvers
required: false,
description: 'Filters designs by their ID.'
argument :filenames,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
required: false,
description: 'Filters designs by their filename.'
diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
index 593974beb04..80db15755d3 100644
--- a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
+++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
@@ -15,13 +15,13 @@ module Resolvers
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
- argument :sha, GraphQL::STRING_TYPE,
+ argument :sha, GraphQL::Types::String,
required: false,
- description: "The SHA256 of a specific version."
+ description: "SHA256 of a specific version."
argument :id, VersionID,
as: :version_id,
required: false,
- description: 'The Global ID of the version.'
+ description: 'Global ID of the version.'
def resolve(version_id: nil, sha: nil)
# TODO: remove this line when the compatibility layer is removed
diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb
index 2144e588208..a6403fbd69f 100644
--- a/app/graphql/resolvers/design_management/version_resolver.rb
+++ b/app/graphql/resolvers/design_management/version_resolver.rb
@@ -11,7 +11,7 @@ module Resolvers
argument :id, ::Types::GlobalIDType[::DesignManagement::Version],
required: true,
- description: 'The Global ID of the version.'
+ description: 'Global ID of the version.'
def resolve(id:)
authorized_find!(id: id)
diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb
index 08b29d884b0..23ba3c86d98 100644
--- a/app/graphql/resolvers/design_management/versions_resolver.rb
+++ b/app/graphql/resolvers/design_management/versions_resolver.rb
@@ -11,15 +11,15 @@ module Resolvers
extras [:parent]
- argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE,
+ argument :earlier_or_equal_to_sha, GraphQL::Types::String,
as: :sha,
required: false,
- description: 'The SHA256 of the most recent acceptable version.'
+ description: 'SHA256 of the most recent acceptable version.'
argument :earlier_or_equal_to_id, VersionID,
as: :id,
required: false,
- description: 'The Global ID of the most recent acceptable version.'
+ description: 'Global ID of the most recent acceptable version.'
# This resolver has a custom singular resolver
def self.single
diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb
index a09b0a1fd87..856e6873a65 100644
--- a/app/graphql/resolvers/echo_resolver.rb
+++ b/app/graphql/resolvers/echo_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class EchoResolver < BaseResolver
- type ::GraphQL::STRING_TYPE, null: false
+ type ::GraphQL::Types::String, null: false
description 'Testing endpoint to validate the API with'
argument :text,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
required: true,
description: 'Text to echo back.'
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
index ee604e7b307..1823eb65d44 100644
--- a/app/graphql/resolvers/environments_resolver.rb
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -2,15 +2,15 @@
module Resolvers
class EnvironmentsResolver < BaseResolver
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: false,
description: 'Name of the environment.'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Search query for environment name.'
- argument :states, [GraphQL::STRING_TYPE],
+ argument :states, [GraphQL::Types::String],
required: false,
description: 'States of environments that should be included in result.'
diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
index 4cd65daa655..793b73342ab 100644
--- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
@@ -6,12 +6,12 @@ module Resolvers
type Types::ErrorTracking::SentryErrorType.connection_type, null: true
extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
- argument :search_term, ::GraphQL::STRING_TYPE,
+ argument :search_term, ::GraphQL::Types::String,
description: 'Search query for the Sentry error details.',
required: false
# TODO: convert to Enum
- argument :sort, ::GraphQL::STRING_TYPE,
+ argument :sort, ::GraphQL::Types::String,
description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
required: false
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
index b5e90da78b2..b8df54f49ab 100644
--- a/app/graphql/resolvers/full_path_resolver.rb
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -5,9 +5,9 @@ module Resolvers
extend ActiveSupport::Concern
prepended do
- argument :full_path, GraphQL::ID_TYPE,
+ argument :full_path, GraphQL::Types::ID,
required: true,
- description: 'The full path of the project, group or namespace, e.g., `gitlab-org/gitlab-foss`.'
+ description: 'Full path of the project, group, or namespace. For example, `gitlab-org/gitlab-foss`.'
end
def model_by_full_path(model, full_path)
diff --git a/app/graphql/resolvers/group_labels_resolver.rb b/app/graphql/resolvers/group_labels_resolver.rb
index 5c2f950bbc0..a22fa9761d6 100644
--- a/app/graphql/resolvers/group_labels_resolver.rb
+++ b/app/graphql/resolvers/group_labels_resolver.rb
@@ -4,12 +4,12 @@ module Resolvers
class GroupLabelsResolver < LabelsResolver
type Types::LabelType.connection_type, null: true
- argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE,
+ argument :include_descendant_groups, GraphQL::Types::Boolean,
required: false,
description: 'Include labels from descendant groups.',
default_value: false
- argument :only_group_labels, GraphQL::BOOLEAN_TYPE,
+ argument :only_group_labels, GraphQL::Types::Boolean,
required: false,
description: 'Include only group level labels.',
default_value: false
diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb
index 31280b36278..44192b6f4bf 100644
--- a/app/graphql/resolvers/group_milestones_resolver.rb
+++ b/app/graphql/resolvers/group_milestones_resolver.rb
@@ -2,10 +2,10 @@
module Resolvers
class GroupMilestonesResolver < MilestonesResolver
- argument :include_descendants, GraphQL::BOOLEAN_TYPE,
+ argument :include_descendants, GraphQL::Types::Boolean,
required: false,
description: 'Include milestones from all subgroups and subprojects.'
- argument :include_ancestors, GraphQL::BOOLEAN_TYPE,
+ argument :include_ancestors, GraphQL::Types::Boolean,
required: false,
description: 'Include milestones from all parent groups.'
diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb
new file mode 100644
index 00000000000..b090fdc49d4
--- /dev/null
+++ b/app/graphql/resolvers/groups_resolver.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupsResolver < BaseResolver
+ type Types::GroupType, null: true
+
+ argument :include_parent_descendants, GraphQL::Types::Boolean,
+ required: false,
+ description: 'List of descendant groups of the parent group.',
+ default_value: true
+
+ argument :owned, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Limit result to groups owned by authenticated user.'
+
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search query for group name or group full path.'
+
+ alias_method :parent, :object
+
+ def resolve(**args)
+ return [] unless parent.present?
+
+ find_groups(args)
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_groups(args)
+ GroupsFinder
+ .new(context[:current_user], args.merge(parent: parent))
+ .execute
+ .reorder('name ASC')
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 2017eb7decd..47e4e3c0b32 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -37,7 +37,8 @@ module Resolvers
[
{
project: [:project_feature]
- }
+ },
+ :author
]
end
diff --git a/app/graphql/resolvers/labels_resolver.rb b/app/graphql/resolvers/labels_resolver.rb
index 1b523b8a240..505d1dff8d2 100644
--- a/app/graphql/resolvers/labels_resolver.rb
+++ b/app/graphql/resolvers/labels_resolver.rb
@@ -8,11 +8,11 @@ module Resolvers
type Types::LabelType.connection_type, null: true
- argument :search_term, GraphQL::STRING_TYPE,
+ argument :search_term, GraphQL::Types::String,
required: false,
description: 'A search term to find labels with.'
- argument :include_ancestor_groups, GraphQL::BOOLEAN_TYPE,
+ argument :include_ancestor_groups, GraphQL::Types::Boolean,
required: false,
description: 'Include labels from ancestor groups.',
default_value: false
diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb
index 2b731d54cdd..827db54134a 100644
--- a/app/graphql/resolvers/members_resolver.rb
+++ b/app/graphql/resolvers/members_resolver.rb
@@ -7,7 +7,7 @@ module Resolvers
type Types::MemberInterface.connection_type, null: true
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Search query.'
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
index c431d079beb..0f0f3ee3772 100644
--- a/app/graphql/resolvers/merge_request_resolver.rb
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -8,7 +8,7 @@ module Resolvers
type ::Types::MergeRequestType, null: true
- argument :iid, GraphQL::STRING_TYPE,
+ argument :iid, GraphQL::Types::String,
required: true,
as: :iids,
description: 'IID of the merge request, for example `1`.'
diff --git a/app/graphql/resolvers/merge_requests_count_resolver.rb b/app/graphql/resolvers/merge_requests_count_resolver.rb
new file mode 100644
index 00000000000..fc5b442db03
--- /dev/null
+++ b/app/graphql/resolvers/merge_requests_count_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class MergeRequestsCountResolver < BaseResolver
+ type GraphQL::Types::Int, null: true
+
+ def resolve
+ BatchLoader::GraphQL.for(object.id).batch do |ids, loader, args|
+ counts = MergeRequestsClosingIssues.count_for_collection(ids, context[:current_user]).to_h
+
+ ids.each do |id|
+ loader.call(id, counts[id] || 0)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index a9eea4ae4b8..8f2c7847a2e 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -10,28 +10,28 @@ module Resolvers
alias_method :project, :object
def self.accept_assignee
- argument :assignee_username, GraphQL::STRING_TYPE,
+ argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of the assignee.'
end
def self.accept_author
- argument :author_username, GraphQL::STRING_TYPE,
+ argument :author_username, GraphQL::Types::String,
required: false,
description: 'Username of the author.'
end
def self.accept_reviewer
- argument :reviewer_username, GraphQL::STRING_TYPE,
+ argument :reviewer_username, GraphQL::Types::String,
required: false,
description: 'Username of the reviewer.'
end
- argument :iids, [GraphQL::STRING_TYPE],
+ argument :iids, [GraphQL::Types::String],
required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
- argument :source_branches, [GraphQL::STRING_TYPE],
+ argument :source_branches, [GraphQL::Types::String],
required: false,
as: :source_branch,
description: <<~DESC
@@ -39,7 +39,7 @@ module Resolvers
All resolved merge requests will have one of these branches as their source.
DESC
- argument :target_branches, [GraphQL::STRING_TYPE],
+ argument :target_branches, [GraphQL::Types::String],
required: false,
as: :target_branch,
description: <<~DESC
@@ -51,7 +51,7 @@ module Resolvers
required: false,
description: 'A merge request state. If provided, all resolved merge requests will have this state.'
- argument :labels, [GraphQL::STRING_TYPE],
+ argument :labels, [GraphQL::Types::String],
required: false,
as: :label_name,
description: 'Array of label names. All resolved merge requests will have all of these labels.'
@@ -61,7 +61,7 @@ module Resolvers
argument :merged_before, Types::TimeType,
required: false,
description: 'Merge requests merged before this date.'
- argument :milestone_title, GraphQL::STRING_TYPE,
+ argument :milestone_title, GraphQL::Types::String,
required: false,
description: 'Title of the milestone.'
argument :sort, Types::MergeRequestSortEnum,
@@ -70,11 +70,11 @@ module Resolvers
default_value: :created_desc
negated do
- argument :labels, [GraphQL::STRING_TYPE],
+ argument :labels, [GraphQL::Types::String],
required: false,
as: :label_name,
description: 'Array of label names. All resolved merge requests will not have these labels.'
- argument :milestone_title, GraphQL::STRING_TYPE,
+ argument :milestone_title, GraphQL::Types::String,
required: false,
description: 'Title of the milestone.'
end
diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb
index 0669fececd5..d2be9fcdd89 100644
--- a/app/graphql/resolvers/metrics/dashboard_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb
@@ -6,7 +6,7 @@ module Resolvers
type Types::Metrics::DashboardType, null: true
calls_gitaly!
- argument :path, GraphQL::STRING_TYPE,
+ argument :path, GraphQL::Types::String,
required: true,
description: <<~MD
Path to a file which defines a metrics dashboard eg: `"config/prometheus/common_metrics.yml"`.
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
index 4fa4c939a23..84f7d66ec19 100644
--- a/app/graphql/resolvers/milestones_resolver.rb
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -5,7 +5,7 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource
include TimeFrameArguments
- argument :ids, [GraphQL::ID_TYPE],
+ argument :ids, [GraphQL::Types::ID],
required: false,
description: 'Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`.'
@@ -13,11 +13,11 @@ module Resolvers
required: false,
description: 'Filter milestones by state.'
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: false,
description: 'The title of the milestone.'
- argument :search_title, GraphQL::STRING_TYPE,
+ argument :search_title, GraphQL::Types::String,
required: false,
description: 'A search string for the title.'
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
index 86286a744bd..c3c61d31e8d 100644
--- a/app/graphql/resolvers/namespace_projects_resolver.rb
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -2,12 +2,12 @@
module Resolvers
class NamespaceProjectsResolver < BaseResolver
- argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
+ argument :include_subgroups, GraphQL::Types::Boolean,
required: false,
default_value: false,
description: 'Include also subgroup projects.'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
default_value: nil,
description: 'Search project with most similar names or paths.'
@@ -17,7 +17,7 @@ module Resolvers
default_value: nil,
description: 'Sort projects by this criteria.'
- argument :ids, [GraphQL::ID_TYPE],
+ argument :ids, [GraphQL::Types::ID],
required: false,
default_value: nil,
description: 'Filter projects by IDs.'
diff --git a/app/graphql/resolvers/packages_base_resolver.rb b/app/graphql/resolvers/packages_base_resolver.rb
index 3378cc32c9f..7d153d16910 100644
--- a/app/graphql/resolvers/packages_base_resolver.rb
+++ b/app/graphql/resolvers/packages_base_resolver.rb
@@ -9,7 +9,7 @@ module Resolvers
required: false,
default_value: :created_desc
- argument :package_name, GraphQL::STRING_TYPE,
+ argument :package_name, GraphQL::Types::String,
description: 'Search a package by name.',
required: false,
default_value: nil
@@ -24,7 +24,7 @@ module Resolvers
required: false,
default_value: nil
- argument :include_versionless, GraphQL::BOOLEAN_TYPE,
+ argument :include_versionless, GraphQL::Types::Boolean,
description: 'Include versionless packages.',
required: false,
default_value: false
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
new file mode 100644
index 00000000000..d1b4e75169c
--- /dev/null
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class PaginatedTreeResolver < BaseResolver
+ type Types::Tree::TreeType.connection_type, null: true
+ extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
+
+ calls_gitaly!
+
+ argument :path, GraphQL::Types::String,
+ required: false,
+ default_value: '', # root of the repository
+ description: 'The path to get the tree for. Default value is the root of the repository.'
+ argument :ref, GraphQL::Types::String,
+ required: false,
+ default_value: :head,
+ description: 'The commit ref to get the tree for. Default value is HEAD.'
+ argument :recursive, GraphQL::Types::Boolean,
+ required: false,
+ default_value: false,
+ description: 'Used to get a recursive tree. Default is false.'
+
+ alias_method :repository, :object
+
+ def resolve(**args)
+ return unless repository.exists?
+
+ cursor = args.delete(:after)
+
+ pagination_params = {
+ limit: @field.max_page_size || 100,
+ page_token: cursor
+ }
+
+ tree = repository.tree(args[:ref], args[:path], recursive: args[:recursive], pagination_params: pagination_params)
+
+ next_cursor = tree.cursor&.next_cursor
+ Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)
+ rescue Gitlab::Git::CommandError => e
+ raise Gitlab::Graphql::Errors::ArgumentError, e
+ end
+
+ def self.field_options
+ super.merge(connection: false) # we manage the pagination manually, so opt out of the connection field extension
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb
index 4cd9cb53f56..567a55aa09b 100644
--- a/app/graphql/resolvers/project_milestones_resolver.rb
+++ b/app/graphql/resolvers/project_milestones_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
class ProjectMilestonesResolver < MilestonesResolver
- argument :include_ancestors, GraphQL::BOOLEAN_TYPE,
+ argument :include_ancestors, GraphQL::Types::Boolean,
required: false,
description: "Also return milestones in the project's parent group and its ancestors."
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index aa8808b15ac..ce4b6ac6b0c 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -6,11 +6,11 @@ module Resolvers
alias_method :project, :object
- argument :iid, GraphQL::ID_TYPE,
+ argument :iid, GraphQL::Types::ID,
required: false,
description: 'IID of the Pipeline. For example, "1".'
- argument :sha, GraphQL::STRING_TYPE,
+ argument :sha, GraphQL::Types::String,
required: false,
description: 'SHA of the Pipeline. For example, "dyd0f15ay83993f5ab66k927w28673882x99100b".'
diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb
index 864acb6d759..56432585015 100644
--- a/app/graphql/resolvers/projects/jira_projects_resolver.rb
+++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb
@@ -9,7 +9,7 @@ module Resolvers
authorize :admin_project
argument :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: false,
description: 'Project name or key.'
diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb
index 3674aa97e1f..99de4df945c 100644
--- a/app/graphql/resolvers/projects/services_resolver.rb
+++ b/app/graphql/resolvers/projects/services_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
authorizes_object!
argument :active,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: 'Indicates if the integration is active.'
argument :type,
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index 665ec796cd3..b846248458f 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -4,27 +4,27 @@ module Resolvers
class ProjectsResolver < BaseResolver
type Types::ProjectType, null: true
- argument :membership, GraphQL::BOOLEAN_TYPE,
+ argument :membership, GraphQL::Types::Boolean,
required: false,
description: 'Limit projects that the current user is a member of.'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Search query for project name, path, or description.'
- argument :ids, [GraphQL::ID_TYPE],
+ argument :ids, [GraphQL::Types::ID],
required: false,
description: 'Filter projects by IDs.'
- argument :search_namespaces, GraphQL::BOOLEAN_TYPE,
+ argument :search_namespaces, GraphQL::Types::Boolean,
required: false,
description: 'Include namespace in project search.'
- argument :sort, GraphQL::STRING_TYPE,
+ argument :sort, GraphQL::Types::String,
required: false,
description: 'Sort order of results.'
- argument :topics, type: [GraphQL::STRING_TYPE],
+ argument :topics, type: [GraphQL::Types::String],
required: false,
description: 'Filters projects by topics.'
diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb
index 67ff5fed0bb..0374a1103de 100644
--- a/app/graphql/resolvers/release_resolver.rb
+++ b/app/graphql/resolvers/release_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class ReleaseResolver < BaseResolver
type Types::ReleaseType, null: true
- argument :tag_name, GraphQL::STRING_TYPE,
+ argument :tag_name, GraphQL::Types::String,
required: true,
description: 'The name of the tag associated to the release.'
diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb
index c0a5ea0366f..e9aacda2652 100644
--- a/app/graphql/resolvers/repository_branch_names_resolver.rb
+++ b/app/graphql/resolvers/repository_branch_names_resolver.rb
@@ -2,19 +2,19 @@
module Resolvers
class RepositoryBranchNamesResolver < BaseResolver
- type ::GraphQL::STRING_TYPE, null: false
+ type ::GraphQL::Types::String, null: false
calls_gitaly!
- argument :search_pattern, GraphQL::STRING_TYPE,
+ argument :search_pattern, GraphQL::Types::String,
required: true,
description: 'The pattern to search for branch names by.'
- argument :offset, GraphQL::INT_TYPE,
+ argument :offset, GraphQL::Types::Int,
required: true,
description: 'The number of branch names to skip.'
- argument :limit, GraphQL::INT_TYPE,
+ argument :limit, GraphQL::Types::Int,
required: true,
description: 'The number of branch names to return.'
diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb
index 4328d38d485..00f41517422 100644
--- a/app/graphql/resolvers/snippets/blobs_resolver.rb
+++ b/app/graphql/resolvers/snippets/blobs_resolver.rb
@@ -12,7 +12,7 @@ module Resolvers
alias_method :snippet, :object
- argument :paths, [GraphQL::STRING_TYPE],
+ argument :paths, [GraphQL::Types::String],
required: false,
description: 'Paths of the blobs.'
diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb
index 7153c919062..7d18c9c6fea 100644
--- a/app/graphql/resolvers/snippets_resolver.rb
+++ b/app/graphql/resolvers/snippets_resolver.rb
@@ -3,6 +3,7 @@
module Resolvers
class SnippetsResolver < BaseResolver
+ include ResolvesIds
include ResolvesSnippets
ERROR_MESSAGE = 'Filtering by both an author and a project is not supported'
@@ -22,7 +23,7 @@ module Resolvers
description: 'The type of snippet.'
argument :explore,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
required: false,
description: 'Explore personal snippets.'
diff --git a/app/graphql/resolvers/terraform/states_resolver.rb b/app/graphql/resolvers/terraform/states_resolver.rb
index f543eb182e8..69785cfc794 100644
--- a/app/graphql/resolvers/terraform/states_resolver.rb
+++ b/app/graphql/resolvers/terraform/states_resolver.rb
@@ -8,7 +8,7 @@ module Resolvers
alias_method :project, :object
when_single do
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: true,
description: 'Name of the Terraform state.'
end
diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb
index 8ac30c4cb5d..14831a29d90 100644
--- a/app/graphql/resolvers/timelog_resolver.rb
+++ b/app/graphql/resolvers/timelog_resolver.rb
@@ -3,33 +3,50 @@
module Resolvers
class TimelogResolver < BaseResolver
include LooksAhead
+ include ResolvesIds
type ::Types::TimelogType.connection_type, null: false
argument :start_date, Types::TimeType,
required: false,
- description: 'List time logs within a date range where the logged date is equal to or after startDate.'
+ description: 'List timelogs within a date range where the logged date is equal to or after startDate.'
argument :end_date, Types::TimeType,
required: false,
- description: 'List time logs within a date range where the logged date is equal to or before endDate.'
+ description: 'List timelogs within a date range where the logged date is equal to or before endDate.'
argument :start_time, Types::TimeType,
required: false,
- description: 'List time-logs within a time range where the logged time is equal to or after startTime.'
+ description: 'List timelogs within a time range where the logged time is equal to or after startTime.'
argument :end_time, Types::TimeType,
required: false,
- description: 'List time-logs within a time range where the logged time is equal to or before endTime.'
+ description: 'List timelogs within a time range where the logged time is equal to or before endTime.'
+
+ argument :project_id, ::Types::GlobalIDType[::Project],
+ required: false,
+ description: 'List timelogs for a project.'
+
+ argument :group_id, ::Types::GlobalIDType[::Group],
+ required: false,
+ description: 'List timelogs for a group.'
+
+ argument :username, GraphQL::Types::String,
+ required: false,
+ description: 'List timelogs for a user.'
def resolve_with_lookahead(**args)
- build_timelogs
+ validate_args!(object, args)
+
+ timelogs = object&.timelogs || Timelog.limit(GitlabSchema.default_max_page_size)
if args.any?
- validate_args!(args)
- build_parsed_args(args)
- validate_time_difference!
- apply_time_filter
+ args = parse_datetime_args(args)
+
+ timelogs = apply_user_filter(timelogs, args)
+ timelogs = apply_project_filter(timelogs, args)
+ timelogs = apply_time_filter(timelogs, args)
+ timelogs = apply_group_filter(timelogs, args)
end
apply_lookahead(timelogs)
@@ -37,30 +54,32 @@ module Resolvers
private
- attr_reader :parsed_args, :timelogs
-
def preloads
{
note: [:note]
}
end
- def validate_args!(args)
- if args[:start_time] && args[:start_date]
+ def validate_args!(object, args)
+ if args.empty? && object.nil?
+ raise_argument_error('Provide at least one argument')
+ elsif args[:start_time] && args[:start_date]
raise_argument_error('Provide either a start date or time, but not both')
elsif args[:end_time] && args[:end_date]
raise_argument_error('Provide either an end date or time, but not both')
end
end
- def build_parsed_args(args)
+ def parse_datetime_args(args)
if times_provided?(args)
- @parsed_args = args
+ args
else
- @parsed_args = args.except(:start_date, :end_date)
+ parsed_args = args.except(:start_date, :end_date)
- @parsed_args[:start_time] = args[:start_date].beginning_of_day if args[:start_date]
- @parsed_args[:end_time] = args[:end_date].end_of_day if args[:end_date]
+ parsed_args[:start_time] = args[:start_date].beginning_of_day if args[:start_date]
+ parsed_args[:end_time] = args[:end_date].end_of_day if args[:end_date]
+
+ parsed_args
end
end
@@ -68,23 +87,51 @@ module Resolvers
args[:start_time] && args[:end_time]
end
- def validate_time_difference!
- return unless end_time_before_start_time?
+ def validate_time_difference!(args)
+ return unless end_time_before_start_time?(args)
raise_argument_error('Start argument must be before End argument')
end
- def end_time_before_start_time?
- times_provided?(parsed_args) && parsed_args[:end_time] < parsed_args[:start_time]
+ def end_time_before_start_time?(args)
+ times_provided?(args) && args[:end_time] < args[:start_time]
end
- def build_timelogs
- @timelogs = Timelog.in_group(object)
+ def apply_project_filter(timelogs, args)
+ return timelogs unless args[:project_id]
+
+ project = resolve_ids(args[:project_id], ::Types::GlobalIDType[::Project])
+ timelogs.in_project(project)
end
- def apply_time_filter
- @timelogs = timelogs.at_or_after(parsed_args[:start_time]) if parsed_args[:start_time]
- @timelogs = timelogs.at_or_before(parsed_args[:end_time]) if parsed_args[:end_time]
+ def apply_group_filter(timelogs, args)
+ return timelogs unless args[:group_id]
+
+ group = Group.find_by_id(resolve_ids(args[:group_id], ::Types::GlobalIDType[::Group]))
+ timelogs.in_group(group)
+ end
+
+ def apply_user_filter(timelogs, args)
+ return timelogs unless args[:username]
+
+ user = UserFinder.new(args[:username]).find_by_username!
+ timelogs.for_user(user)
+ end
+
+ def apply_time_filter(timelogs, args)
+ return timelogs unless args[:start_time] || args[:end_time]
+
+ validate_time_difference!(args)
+
+ if args[:start_time]
+ timelogs = timelogs.at_or_after(args[:start_time])
+ end
+
+ if args[:end_time]
+ timelogs = timelogs.at_or_before(args[:end_time])
+ end
+
+ timelogs
end
def raise_argument_error(message)
diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb
index af48ceefd6f..263b190c74e 100644
--- a/app/graphql/resolvers/todo_resolver.rb
+++ b/app/graphql/resolvers/todo_resolver.rb
@@ -10,15 +10,15 @@ module Resolvers
required: false,
description: 'The action to be filtered.'
- argument :author_id, [GraphQL::ID_TYPE],
+ argument :author_id, [GraphQL::Types::ID],
required: false,
description: 'The ID of an author.'
- argument :project_id, [GraphQL::ID_TYPE],
+ argument :project_id, [GraphQL::Types::ID],
required: false,
description: 'The ID of a project.'
- argument :group_id, [GraphQL::ID_TYPE],
+ argument :group_id, [GraphQL::Types::ID],
required: false,
description: 'The ID of a group.'
diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb
index c07d9187d4d..70b4d81845c 100644
--- a/app/graphql/resolvers/tree_resolver.rb
+++ b/app/graphql/resolvers/tree_resolver.rb
@@ -6,15 +6,15 @@ module Resolvers
calls_gitaly!
- argument :path, GraphQL::STRING_TYPE,
+ argument :path, GraphQL::Types::String,
required: false,
default_value: '',
description: 'The path to get the tree for. Default value is the root of the repository.'
- argument :ref, GraphQL::STRING_TYPE,
+ argument :ref, GraphQL::Types::String,
required: false,
default_value: :head,
description: 'The commit ref to get the tree for. Default value is HEAD.'
- argument :recursive, GraphQL::BOOLEAN_TYPE,
+ argument :recursive, GraphQL::Types::Boolean,
required: false,
default_value: false,
description: 'Used to get a recursive tree. Default is false.'
diff --git a/app/graphql/resolvers/user_discussions_count_resolver.rb b/app/graphql/resolvers/user_discussions_count_resolver.rb
index 115997ec666..0a04e8df0b8 100644
--- a/app/graphql/resolvers/user_discussions_count_resolver.rb
+++ b/app/graphql/resolvers/user_discussions_count_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class UserDiscussionsCountResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
- type GraphQL::INT_TYPE, null: true
+ type GraphQL::Types::Int, null: true
def resolve
authorize!(object)
diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
index 221a43f691d..e0201e45664 100644
--- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb
+++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
@@ -5,7 +5,7 @@ module Resolvers
include ResolvesProject
argument :project_path,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
required: false,
description: <<~DESC
The full-path of the project the authored merge requests should be in.
diff --git a/app/graphql/resolvers/user_notes_count_resolver.rb b/app/graphql/resolvers/user_notes_count_resolver.rb
index 2cb61104c18..b91815c72f5 100644
--- a/app/graphql/resolvers/user_notes_count_resolver.rb
+++ b/app/graphql/resolvers/user_notes_count_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class UserNotesCountResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
- type GraphQL::INT_TYPE, null: true
+ type GraphQL::Types::Int, null: true
def resolve
authorize!(object)
diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb
index 84bc03091d9..99fd0d4927d 100644
--- a/app/graphql/resolvers/user_resolver.rb
+++ b/app/graphql/resolvers/user_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
required: false,
description: 'ID of the User.'
- argument :username, GraphQL::STRING_TYPE,
+ argument :username, GraphQL::Types::String,
required: false,
description: 'Username of the User.'
diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb
index a8abe759f27..07c6611e41d 100644
--- a/app/graphql/resolvers/user_starred_projects_resolver.rb
+++ b/app/graphql/resolvers/user_starred_projects_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class UserStarredProjectsResolver < BaseResolver
type Types::ProjectType.connection_type, null: true
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Search query.'
diff --git a/app/graphql/resolvers/users/group_count_resolver.rb b/app/graphql/resolvers/users/group_count_resolver.rb
index ebfe594d31d..592436ac546 100644
--- a/app/graphql/resolvers/users/group_count_resolver.rb
+++ b/app/graphql/resolvers/users/group_count_resolver.rb
@@ -3,7 +3,7 @@
module Resolvers
module Users
class GroupCountResolver < BaseResolver
- type GraphQL::INT_TYPE, null: true
+ type GraphQL::Types::Int, null: true
alias_method :user, :object
diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb
index 95ced131504..c6de3dba41a 100644
--- a/app/graphql/resolvers/users_resolver.rb
+++ b/app/graphql/resolvers/users_resolver.rb
@@ -7,11 +7,11 @@ module Resolvers
type Types::UserType.connection_type, null: true
description 'Find Users'
- argument :ids, [GraphQL::ID_TYPE],
+ argument :ids, [GraphQL::Types::ID],
required: false,
description: 'List of user Global IDs.'
- argument :usernames, [GraphQL::STRING_TYPE], required: false,
+ argument :usernames, [GraphQL::Types::String], required: false,
description: 'List of usernames.'
argument :sort, Types::SortEnum,
@@ -19,11 +19,11 @@ module Resolvers
required: false,
default_value: :created_desc
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: "Query to search users by name, username, or primary email."
- argument :admins, GraphQL::BOOLEAN_TYPE,
+ argument :admins, GraphQL::Types::Boolean,
required: false,
default_value: false,
description: 'Return only admin users.'
diff --git a/app/graphql/types/access_level_type.rb b/app/graphql/types/access_level_type.rb
index 21c3669979c..2d97f6b30e8 100644
--- a/app/graphql/types/access_level_type.rb
+++ b/app/graphql/types/access_level_type.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'AccessLevel'
description 'Represents the access level of a relationship between a User and object that it is related to'
- field :integer_value, GraphQL::INT_TYPE, null: true,
+ field :integer_value, GraphQL::Types::Int, null: true,
description: 'Integer representation of access level.',
method: :to_i
diff --git a/app/graphql/types/admin/analytics/usage_trends/measurement_type.rb b/app/graphql/types/admin/analytics/usage_trends/measurement_type.rb
index c8ca702706f..c54c938402d 100644
--- a/app/graphql/types/admin/analytics/usage_trends/measurement_type.rb
+++ b/app/graphql/types/admin/analytics/usage_trends/measurement_type.rb
@@ -14,7 +14,7 @@ module Types
field :recorded_at, Types::TimeType, null: true,
description: 'The time the measurement was recorded.'
- field :count, GraphQL::INT_TYPE, null: false,
+ field :count, GraphQL::Types::Int, null: false,
description: 'Object count.'
field :identifier, Types::Admin::Analytics::UsageTrends::MeasurementIdentifierEnum, null: false,
diff --git a/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb b/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
index 996300edad3..cc6e3db007b 100644
--- a/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
+++ b/app/graphql/types/admin/sidekiq_queues/delete_jobs_response_type.rb
@@ -10,17 +10,17 @@ module Types
description 'The response from the AdminSidekiqQueuesDeleteJobs mutation'
field :completed,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
null: true,
description: 'Whether or not the entire queue was processed in time; if not, retrying the same request is safe.'
field :deleted_jobs,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: 'The number of matching jobs deleted.'
field :queue_size,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: 'The queue size after processing.'
end
diff --git a/app/graphql/types/alert_management/alert_status_counts_type.rb b/app/graphql/types/alert_management/alert_status_counts_type.rb
index 14a81735fa5..c4aedb09997 100644
--- a/app/graphql/types/alert_management/alert_status_counts_type.rb
+++ b/app/graphql/types/alert_management/alert_status_counts_type.rb
@@ -11,18 +11,18 @@ module Types
::AlertManagement::Alert.status_names.each do |status|
field status,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: "Number of alerts with status #{status.to_s.upcase} for the project"
end
field :open,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: 'Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project.'
field :all,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: 'Total number of alerts for the project.'
end
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index eb0d999554f..bdfdd2c5886 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -13,12 +13,12 @@ module Types
authorize :read_alert_management_alert
field :iid,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
null: false,
description: 'Internal ID of the alert.'
field :issue_iid,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
null: true,
deprecated: { reason: 'Use issue field', milestone: '13.10' },
description: 'Internal ID of the GitLab issue attached to the alert.'
@@ -29,12 +29,12 @@ module Types
description: 'Issue attached to the alert.'
field :title,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Title of the alert.'
field :description,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Description of the alert.'
@@ -50,17 +50,17 @@ module Types
method: :status_name
field :service,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Service the alert came from.'
field :monitoring_tool,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Monitoring tool the alert came from.'
field :hosts,
- [GraphQL::STRING_TYPE],
+ [GraphQL::Types::String],
null: true,
description: 'List of hosts the alert came from.'
@@ -80,7 +80,7 @@ module Types
description: 'Environment for the alert.'
field :event_count,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: 'Number of events of this alert.',
method: :events
@@ -106,12 +106,12 @@ module Types
description: 'Assignees of the alert.'
field :metrics_dashboard_url,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'URL for metrics embed for the alert.'
field :runbook,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Runbook for the alert as defined in alert details.'
@@ -120,7 +120,7 @@ module Types
end
field :details_url,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The URL of the alert detail page.'
diff --git a/app/graphql/types/alert_management/integration_type.rb b/app/graphql/types/alert_management/integration_type.rb
index 6cbc17cdbfb..ca7ec99a480 100644
--- a/app/graphql/types/alert_management/integration_type.rb
+++ b/app/graphql/types/alert_management/integration_type.rb
@@ -7,7 +7,7 @@ module Types
graphql_name 'AlertManagementIntegration'
field :id,
- GraphQL::ID_TYPE,
+ GraphQL::Types::ID,
null: false,
description: 'ID of the integration.'
@@ -17,27 +17,27 @@ module Types
description: 'Type of integration.'
field :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Name of the integration.'
field :active,
- GraphQL::BOOLEAN_TYPE,
+ GraphQL::Types::Boolean,
null: true,
description: 'Whether the endpoint is currently accepting alerts.'
field :token,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Token used to authenticate alert notification requests.'
field :url,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'Endpoint which accepts alert notifications.'
field :api_url,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: 'URL at which Prometheus metrics can be queried to populate the metrics dashboard.'
diff --git a/app/graphql/types/award_emojis/award_emoji_type.rb b/app/graphql/types/award_emojis/award_emoji_type.rb
index 9409304e28f..1f6f0badcac 100644
--- a/app/graphql/types/award_emojis/award_emoji_type.rb
+++ b/app/graphql/types/award_emojis/award_emoji_type.rb
@@ -11,27 +11,27 @@ module Types
present_using AwardEmojiPresenter
field :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The emoji name.'
field :description,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The emoji description.'
field :unicode,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The emoji in Unicode.'
field :emoji,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The emoji as an icon.'
field :unicode_version,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The Unicode version for this emoji.'
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index 536a32f2bdd..2c899e9edaa 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -10,7 +10,29 @@ module Types
@deprecation = gitlab_deprecation(kwargs)
@doc_reference = kwargs.delete(:see)
+ # our custom addition `nullable` which allows us to declare
+ # an argument that must be provided, even if its value is null.
+ # When `required: true` then required arguments must not be null.
+ @gl_required = !!kwargs[:required]
+ @gl_nullable = kwargs[:required] == :nullable
+
+ # Only valid if an argument is also required.
+ if @gl_nullable
+ # Since the framework asserts that "required" means "cannot be null"
+ # we have to switch off "required" but still do the check in `ready?` behind the scenes
+ kwargs[:required] = false
+ end
+
super(*args, **kwargs, &block)
end
+
+ def accepts?(value)
+ # if the argument is declared as required, it must be included
+ return false if @gl_required && value == :not_given
+ # if the argument is declared as required, the value can only be null IF it is also nullable.
+ return false if @gl_required && value.nil? && !@gl_nullable
+
+ true
+ end
end
end
diff --git a/app/graphql/types/blob_viewer_type.rb b/app/graphql/types/blob_viewer_type.rb
index 8d863c32bc7..8c565373cfd 100644
--- a/app/graphql/types/blob_viewer_type.rb
+++ b/app/graphql/types/blob_viewer_type.rb
@@ -9,30 +9,30 @@ module Types
description: 'Type of blob viewer.',
null: false
- field :load_async, GraphQL::BOOLEAN_TYPE,
+ field :load_async, GraphQL::Types::Boolean,
description: 'Shows whether the blob content is loaded asynchronously.',
null: false
- field :collapsed, GraphQL::BOOLEAN_TYPE,
+ field :collapsed, GraphQL::Types::Boolean,
description: 'Shows whether the blob should be displayed collapsed.',
method: :collapsed?,
null: false
- field :too_large, GraphQL::BOOLEAN_TYPE,
+ field :too_large, GraphQL::Types::Boolean,
description: 'Shows whether the blob is too large to be displayed.',
method: :too_large?,
null: false
- field :render_error, GraphQL::STRING_TYPE,
+ field :render_error, GraphQL::Types::String,
description: 'Error rendering the blob content.',
null: true
- field :file_type, GraphQL::STRING_TYPE,
+ field :file_type, GraphQL::Types::String,
description: 'Content file type.',
method: :partial_name,
null: false
- field :loading_partial_name, GraphQL::STRING_TYPE,
+ field :loading_partial_name, GraphQL::Types::String,
description: 'Loading partial name.',
null: false
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
index dc10716dcb0..762e03973d9 100644
--- a/app/graphql/types/board_list_type.rb
+++ b/app/graphql/types/board_list_type.rb
@@ -10,19 +10,19 @@ module Types
alias_method :list, :object
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID (global ID) of the list.'
- field :title, GraphQL::STRING_TYPE, null: false,
+ field :title, GraphQL::Types::String, null: false,
description: 'Title of the list.'
- field :list_type, GraphQL::STRING_TYPE, null: false,
+ field :list_type, GraphQL::Types::String, null: false,
description: 'Type of the list.'
- field :position, GraphQL::INT_TYPE, null: true,
+ field :position, GraphQL::Types::Int, null: true,
description: 'Position of list within the board.'
field :label, Types::LabelType, null: true,
description: 'Label of the list.'
- field :collapsed, GraphQL::BOOLEAN_TYPE, null: true,
+ field :collapsed, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the list is collapsed for this user.'
- field :issues_count, GraphQL::INT_TYPE, null: true,
+ field :issues_count, GraphQL::Types::Int, null: true,
description: 'Count of issues in the list.'
field :issues, ::Types::IssueType.connection_type, null: true,
diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb
index 292809b0d64..4ec9a8a9c63 100644
--- a/app/graphql/types/board_type.rb
+++ b/app/graphql/types/board_type.rb
@@ -9,15 +9,15 @@ module Types
present_using BoardPresenter
- field :id, type: GraphQL::ID_TYPE, null: false,
+ field :id, type: GraphQL::Types::ID, null: false,
description: 'ID (global ID) of the board.'
- field :name, type: GraphQL::STRING_TYPE, null: true,
+ field :name, type: GraphQL::Types::String, null: true,
description: 'Name of the board.'
- field :hide_backlog_list, type: GraphQL::BOOLEAN_TYPE, null: true,
+ field :hide_backlog_list, type: GraphQL::Types::Boolean, null: true,
description: 'Whether or not backlog list is hidden.'
- field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
+ field :hide_closed_list, type: GraphQL::Types::Boolean, null: true,
description: 'Whether or not closed list is hidden.'
field :created_at, Types::TimeType, null: false,
@@ -33,10 +33,10 @@ module Types
resolver: Resolvers::BoardListsResolver,
extras: [:lookahead]
- field :web_path, GraphQL::STRING_TYPE, null: false,
+ field :web_path, GraphQL::Types::String, null: false,
description: 'Web path of the board.'
- field :web_url, GraphQL::STRING_TYPE, null: false,
+ field :web_url, GraphQL::Types::String, null: false,
description: 'Web URL of the board.'
end
end
diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb
index 2cd057347d6..326f73846d0 100644
--- a/app/graphql/types/boards/board_issuable_input_base_type.rb
+++ b/app/graphql/types/boards/board_issuable_input_base_type.rb
@@ -4,15 +4,15 @@ module Types
module Boards
# Common arguments that we can be used to filter boards epics and issues
class BoardIssuableInputBaseType < BaseInputObject
- argument :label_name, [GraphQL::STRING_TYPE, null: true],
+ argument :label_name, [GraphQL::Types::String, null: true],
required: false,
description: 'Filter by label name.'
- argument :author_username, GraphQL::STRING_TYPE,
+ argument :author_username, GraphQL::Types::String,
required: false,
description: 'Filter by author username.'
- argument :my_reaction_emoji, GraphQL::STRING_TYPE,
+ argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user.'
end
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 30f4efcd403..82db1802b81 100644
--- a/app/graphql/types/boards/board_issue_input_base_type.rb
+++ b/app/graphql/types/boards/board_issue_input_base_type.rb
@@ -4,21 +4,26 @@ module Types
module Boards
# rubocop: disable Graphql/AuthorizeTypes
class BoardIssueInputBaseType < BoardIssuableInputBaseType
- argument :iids, [GraphQL::STRING_TYPE],
+ argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues. For example `["1", "2"]`.'
- argument :milestone_title, GraphQL::STRING_TYPE,
+ argument :milestone_title, GraphQL::Types::String,
required: false,
description: 'Filter by milestone title.'
- argument :assignee_username, [GraphQL::STRING_TYPE, null: true],
+ argument :assignee_username, [GraphQL::Types::String, null: true],
required: false,
description: 'Filter by assignee username.'
- argument :release_tag, GraphQL::STRING_TYPE,
+ argument :release_tag, GraphQL::Types::String,
required: false,
description: 'Filter by release tag.'
+
+ argument :types, [Types::IssueTypeEnum],
+ as: :issue_types,
+ description: 'Filter by the given issue types.',
+ required: false
end
end
end
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
index 7580b0378fe..b4dbe87e32d 100644
--- a/app/graphql/types/boards/board_issue_input_type.rb
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -10,7 +10,7 @@ module Types
prepare: ->(negated_args, ctx) { negated_args.to_h },
description: 'List of negated arguments.'
- argument :search, GraphQL::STRING_TYPE,
+ argument :search, GraphQL::Types::String,
required: false,
description: 'Search query for issue title or description.'
diff --git a/app/graphql/types/branch_type.rb b/app/graphql/types/branch_type.rb
index b788ba79769..a861bcc4bb4 100644
--- a/app/graphql/types/branch_type.rb
+++ b/app/graphql/types/branch_type.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'Branch'
field :name,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'Name of the branch.'
diff --git a/app/graphql/types/ci/analytics_type.rb b/app/graphql/types/ci/analytics_type.rb
index ba987b133bd..f52b9eae229 100644
--- a/app/graphql/types/ci/analytics_type.rb
+++ b/app/graphql/types/ci/analytics_type.rb
@@ -6,27 +6,27 @@ module Types
class AnalyticsType < BaseObject
graphql_name 'PipelineAnalytics'
- field :week_pipelines_totals, [GraphQL::INT_TYPE], null: true,
+ field :week_pipelines_totals, [GraphQL::Types::Int], null: true,
description: 'Total weekly pipeline count.'
- field :week_pipelines_successful, [GraphQL::INT_TYPE], null: true,
+ field :week_pipelines_successful, [GraphQL::Types::Int], null: true,
description: 'Total weekly successful pipeline count.'
- field :week_pipelines_labels, [GraphQL::STRING_TYPE], null: true,
+ field :week_pipelines_labels, [GraphQL::Types::String], null: true,
description: 'Labels for the weekly pipeline count.'
- field :month_pipelines_totals, [GraphQL::INT_TYPE], null: true,
+ field :month_pipelines_totals, [GraphQL::Types::Int], null: true,
description: 'Total monthly pipeline count.'
- field :month_pipelines_successful, [GraphQL::INT_TYPE], null: true,
+ field :month_pipelines_successful, [GraphQL::Types::Int], null: true,
description: 'Total monthly successful pipeline count.'
- field :month_pipelines_labels, [GraphQL::STRING_TYPE], null: true,
+ field :month_pipelines_labels, [GraphQL::Types::String], null: true,
description: 'Labels for the monthly pipeline count.'
- field :year_pipelines_totals, [GraphQL::INT_TYPE], null: true,
+ field :year_pipelines_totals, [GraphQL::Types::Int], null: true,
description: 'Total yearly pipeline count.'
- field :year_pipelines_successful, [GraphQL::INT_TYPE], null: true,
+ field :year_pipelines_successful, [GraphQL::Types::Int], null: true,
description: 'Total yearly successful pipeline count.'
- field :year_pipelines_labels, [GraphQL::STRING_TYPE], null: true,
+ field :year_pipelines_labels, [GraphQL::Types::String], null: true,
description: 'Labels for the yearly pipeline count.'
- field :pipeline_times_values, [GraphQL::INT_TYPE], null: true,
+ field :pipeline_times_values, [GraphQL::Types::Int], null: true,
description: 'Pipeline times.'
- field :pipeline_times_labels, [GraphQL::STRING_TYPE], null: true,
+ field :pipeline_times_labels, [GraphQL::Types::String], null: true,
description: 'Pipeline times labels.'
end
end
diff --git a/app/graphql/types/ci/application_setting_type.rb b/app/graphql/types/ci/application_setting_type.rb
index 8616280057c..2322778d159 100644
--- a/app/graphql/types/ci/application_setting_type.rb
+++ b/app/graphql/types/ci/application_setting_type.rb
@@ -7,7 +7,7 @@ module Types
authorize :read_application_setting
- field :keep_latest_artifact, GraphQL::BOOLEAN_TYPE, null: true,
+ field :keep_latest_artifact, GraphQL::Types::Boolean, null: true,
description: 'Whether to keep the latest jobs artifacts.'
end
end
diff --git a/app/graphql/types/ci/build_need_type.rb b/app/graphql/types/ci/build_need_type.rb
index 19ff758ad1d..7bd12c99a08 100644
--- a/app/graphql/types/ci/build_need_type.rb
+++ b/app/graphql/types/ci/build_need_type.rb
@@ -7,9 +7,9 @@ module Types
class BuildNeedType < BaseObject
graphql_name 'CiBuildNeed'
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the job we need to complete.'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the job we need to complete.'
end
end
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index f90c75454ba..790deab8f68 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -7,16 +7,16 @@ module Types
authorize :admin_project
- field :merge_pipelines_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :merge_pipelines_enabled, GraphQL::Types::Boolean, null: true,
description: 'Whether merge pipelines are enabled.',
method: :merge_pipelines_enabled?
- field :merge_trains_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :merge_trains_enabled, GraphQL::Types::Boolean, null: true,
description: 'Whether merge trains are enabled.',
method: :merge_trains_enabled?
- field :keep_latest_artifact, GraphQL::BOOLEAN_TYPE, null: true,
+ field :keep_latest_artifact, GraphQL::Types::Boolean, null: true,
description: 'Whether to keep the latest builds artifacts.',
method: :keep_latest_artifacts_available?
- field :job_token_scope_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :job_token_scope_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates CI job tokens generated in this project have restricted access to resources.',
method: :job_token_scope_enabled?
field :project, Types::ProjectType, null: true,
diff --git a/app/graphql/types/ci/config/config_type.rb b/app/graphql/types/ci/config/config_type.rb
index 88caf21c376..6ac21968bd4 100644
--- a/app/graphql/types/ci/config/config_type.rb
+++ b/app/graphql/types/ci/config/config_type.rb
@@ -7,9 +7,9 @@ module Types
class ConfigType < BaseObject
graphql_name 'CiConfig'
- field :errors, [GraphQL::STRING_TYPE], null: true,
+ field :errors, [GraphQL::Types::String], null: true,
description: 'Linting errors.'
- field :merged_yaml, GraphQL::STRING_TYPE, null: true,
+ field :merged_yaml, GraphQL::Types::String, null: true,
description: 'Merged CI configuration YAML.'
field :stages, Types::Ci::Config::StageType.connection_type, null: true,
description: 'Stages of the pipeline.'
diff --git a/app/graphql/types/ci/config/group_type.rb b/app/graphql/types/ci/config/group_type.rb
index 11be701e73f..e5cb0d4e72f 100644
--- a/app/graphql/types/ci/config/group_type.rb
+++ b/app/graphql/types/ci/config/group_type.rb
@@ -7,11 +7,11 @@ module Types
class GroupType < BaseObject
graphql_name 'CiConfigGroup'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the job group.'
field :jobs, Types::Ci::Config::JobType.connection_type, null: true,
description: 'Jobs in group.'
- field :size, GraphQL::INT_TYPE, null: true,
+ field :size, GraphQL::Types::Int, null: true,
description: 'Size of the job group.'
end
end
diff --git a/app/graphql/types/ci/config/job_restriction_type.rb b/app/graphql/types/ci/config/job_restriction_type.rb
index 294e3c94571..891ba18dacc 100644
--- a/app/graphql/types/ci/config/job_restriction_type.rb
+++ b/app/graphql/types/ci/config/job_restriction_type.rb
@@ -7,7 +7,7 @@ module Types
class JobRestrictionType < BaseObject
graphql_name 'CiConfigJobRestriction'
- field :refs, [GraphQL::STRING_TYPE], null: true,
+ field :refs, [GraphQL::Types::String], null: true,
description: 'The Git refs the job restriction applies to.'
end
end
diff --git a/app/graphql/types/ci/config/job_type.rb b/app/graphql/types/ci/config/job_type.rb
index 65fdc4c2615..4cf6780ef60 100644
--- a/app/graphql/types/ci/config/job_type.rb
+++ b/app/graphql/types/ci/config/job_type.rb
@@ -7,32 +7,32 @@ module Types
class JobType < BaseObject
graphql_name 'CiConfigJob'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the job.'
- field :group_name, GraphQL::STRING_TYPE, null: true,
+ field :group_name, GraphQL::Types::String, null: true,
description: 'Name of the job group.'
- field :stage, GraphQL::STRING_TYPE, null: true,
+ field :stage, GraphQL::Types::String, null: true,
description: 'Name of the job stage.'
field :needs, Types::Ci::Config::NeedType.connection_type, null: true,
description: 'Builds that must complete before the jobs run.'
- field :allow_failure, GraphQL::BOOLEAN_TYPE, null: true,
+ field :allow_failure, GraphQL::Types::Boolean, null: true,
description: 'Allow job to fail.'
- field :before_script, [GraphQL::STRING_TYPE], null: true,
+ field :before_script, [GraphQL::Types::String], null: true,
description: 'Override a set of commands that are executed before the job.'
- field :script, [GraphQL::STRING_TYPE], null: true,
+ field :script, [GraphQL::Types::String], null: true,
description: 'Shell script that is executed by a runner.'
- field :after_script, [GraphQL::STRING_TYPE], null: true,
+ field :after_script, [GraphQL::Types::String], null: true,
description: 'Override a set of commands that are executed after the job.'
- field :when, GraphQL::STRING_TYPE, null: true,
+ field :when, GraphQL::Types::String, null: true,
description: 'When to run the job.',
resolver_method: :restrict_when_to_run_jobs
- field :environment, GraphQL::STRING_TYPE, null: true,
+ field :environment, GraphQL::Types::String, null: true,
description: 'Name of an environment to which the job deploys.'
field :except, Types::Ci::Config::JobRestrictionType, null: true,
description: 'Limit when jobs are not created.'
field :only, Types::Ci::Config::JobRestrictionType, null: true,
description: 'Jobs are created when these conditions do not apply.'
- field :tags, [GraphQL::STRING_TYPE], null: true,
+ field :tags, [GraphQL::Types::String], null: true,
description: 'List of tags that are used to select a runner.'
def restrict_when_to_run_jobs
diff --git a/app/graphql/types/ci/config/need_type.rb b/app/graphql/types/ci/config/need_type.rb
index 01f73100409..6e9aea8eb64 100644
--- a/app/graphql/types/ci/config/need_type.rb
+++ b/app/graphql/types/ci/config/need_type.rb
@@ -7,7 +7,7 @@ module Types
class NeedType < BaseObject
graphql_name 'CiConfigNeed'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the need.'
end
end
diff --git a/app/graphql/types/ci/config/stage_type.rb b/app/graphql/types/ci/config/stage_type.rb
index 060efb7d45c..7e2aa9470f2 100644
--- a/app/graphql/types/ci/config/stage_type.rb
+++ b/app/graphql/types/ci/config/stage_type.rb
@@ -7,7 +7,7 @@ module Types
class StageType < BaseObject
graphql_name 'CiConfigStage'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the stage.'
field :groups, Types::Ci::Config::GroupType.connection_type, null: true,
description: 'Groups of jobs for the stage.'
diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb
index 6310a62a103..4433e921971 100644
--- a/app/graphql/types/ci/detailed_status_type.rb
+++ b/app/graphql/types/ci/detailed_status_type.rb
@@ -6,26 +6,26 @@ module Types
class DetailedStatusType < BaseObject
graphql_name 'DetailedStatus'
- field :id, GraphQL::STRING_TYPE, null: false,
+ field :id, GraphQL::Types::String, null: false,
description: 'ID for a detailed status.',
extras: [:parent]
- field :group, GraphQL::STRING_TYPE, null: true,
+ field :group, GraphQL::Types::String, null: true,
description: 'Group of the status.'
- field :icon, GraphQL::STRING_TYPE, null: true,
+ field :icon, GraphQL::Types::String, null: true,
description: 'Icon of the status.'
- field :favicon, GraphQL::STRING_TYPE, null: true,
+ field :favicon, GraphQL::Types::String, null: true,
description: 'Favicon of the status.'
- field :details_path, GraphQL::STRING_TYPE, null: true,
+ field :details_path, GraphQL::Types::String, null: true,
description: 'Path of the details for the status.'
- field :has_details, GraphQL::BOOLEAN_TYPE, null: true,
+ field :has_details, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the status has further details.',
method: :has_details?
- field :label, GraphQL::STRING_TYPE, null: true,
+ field :label, GraphQL::Types::String, null: true,
calls_gitaly: true,
description: 'Label of the status.'
- field :text, GraphQL::STRING_TYPE, null: true,
+ field :text, GraphQL::Types::String, null: true,
description: 'Text of the status.'
- field :tooltip, GraphQL::STRING_TYPE, null: true,
+ field :tooltip, GraphQL::Types::String, null: true,
description: 'Tooltip associated with the status.',
method: :status_tooltip
field :action, Types::Ci::StatusActionType, null: true,
diff --git a/app/graphql/types/ci/group_type.rb b/app/graphql/types/ci/group_type.rb
index 3da183cb842..3ae23ba9bd4 100644
--- a/app/graphql/types/ci/group_type.rb
+++ b/app/graphql/types/ci/group_type.rb
@@ -6,11 +6,11 @@ module Types
class GroupType < BaseObject
graphql_name 'CiGroup'
- field :id, GraphQL::STRING_TYPE, null: false,
+ field :id, GraphQL::Types::String, null: false,
description: 'ID for a group.'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the job group.'
- field :size, GraphQL::INT_TYPE, null: true,
+ field :size, GraphQL::Types::Int, null: true,
description: 'Size of the group.'
field :jobs, Ci::JobType.connection_type, null: true,
description: 'Jobs in group.'
diff --git a/app/graphql/types/ci/job_artifact_type.rb b/app/graphql/types/ci/job_artifact_type.rb
index 7dc93041b53..f80681bcf36 100644
--- a/app/graphql/types/ci/job_artifact_type.rb
+++ b/app/graphql/types/ci/job_artifact_type.rb
@@ -6,7 +6,7 @@ module Types
class JobArtifactType < BaseObject
graphql_name 'CiJobArtifact'
- field :download_path, GraphQL::STRING_TYPE, null: true,
+ field :download_path, GraphQL::Types::String, null: true,
description: "URL for downloading the artifact's file."
field :file_type, ::Types::Ci::JobArtifactFileTypeEnum, null: true,
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 360ea3ba7a9..4a3518e1865 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -14,7 +14,7 @@ module Types
description: 'ID of the job.'
field :pipeline, Types::Ci::PipelineType, null: true,
description: 'Pipeline the job belongs to.'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the job.'
field :needs, BuildNeedType.connection_type, null: true,
description: 'References to builds that must complete before the jobs run.'
@@ -24,11 +24,11 @@ module Types
description: "Status of the job."
field :stage, Types::Ci::StageType, null: true,
description: 'Stage of the job.'
- field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
+ field :allow_failure, ::GraphQL::Types::Boolean, null: false,
description: 'Whether the job is allowed to fail.'
- field :duration, GraphQL::INT_TYPE, null: true,
+ field :duration, GraphQL::Types::Int, null: true,
description: 'Duration of the job in seconds.'
- field :tags, [GraphQL::STRING_TYPE], null: true,
+ field :tags, [GraphQL::Types::String], null: true,
description: 'Tags for the current job.'
# Life-cycle timestamps:
@@ -53,33 +53,33 @@ module Types
description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job.'
- field :short_sha, type: GraphQL::STRING_TYPE, null: false,
+ field :short_sha, type: GraphQL::Types::String, null: false,
description: 'Short SHA1 ID of the commit.'
- field :scheduling_type, GraphQL::STRING_TYPE, null: true,
+ field :scheduling_type, GraphQL::Types::String, null: true,
description: 'Type of job scheduling. Value is `dag` if the job uses the `needs` keyword, and `stage` otherwise.'
- field :commit_path, GraphQL::STRING_TYPE, null: true,
+ field :commit_path, GraphQL::Types::String, null: true,
description: 'Path to the commit that triggered the job.'
- field :ref_name, GraphQL::STRING_TYPE, null: true,
+ field :ref_name, GraphQL::Types::String, null: true,
description: 'Ref name of the job.'
- field :ref_path, GraphQL::STRING_TYPE, null: true,
+ field :ref_path, GraphQL::Types::String, null: true,
description: 'Path to the ref.'
- field :playable, GraphQL::BOOLEAN_TYPE, null: false, method: :playable?,
+ field :playable, GraphQL::Types::Boolean, null: false, method: :playable?,
description: 'Indicates the job can be played.'
- field :retryable, GraphQL::BOOLEAN_TYPE, null: false, method: :retryable?,
+ field :retryable, GraphQL::Types::Boolean, null: false, method: :retryable?,
description: 'Indicates the job can be retried.'
- field :cancelable, GraphQL::BOOLEAN_TYPE, null: false, method: :cancelable?,
+ field :cancelable, GraphQL::Types::Boolean, null: false, method: :cancelable?,
description: 'Indicates the job can be canceled.'
- field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?,
+ field :active, GraphQL::Types::Boolean, null: false, method: :active?,
description: 'Indicates the job is active.'
- field :stuck, GraphQL::BOOLEAN_TYPE, null: false, method: :stuck?,
+ field :stuck, GraphQL::Types::Boolean, 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,
+ field :created_by_tag, GraphQL::Types::Boolean, null: false,
description: 'Whether the job was created by a tag.'
- field :manual_job, GraphQL::BOOLEAN_TYPE, null: true,
+ field :manual_job, GraphQL::Types::Boolean, null: true,
description: 'Whether the job has a manual action.'
- field :triggered, GraphQL::BOOLEAN_TYPE, null: true,
+ field :triggered, GraphQL::Types::Boolean, null: true,
description: 'Whether the job was triggered.'
def pipeline
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index f4a6c18f73e..0375257eb7b 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -12,25 +12,25 @@ module Types
expose_permissions Types::PermissionTypes::Ci::Pipeline
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the pipeline.'
- field :iid, GraphQL::STRING_TYPE, null: false,
+ field :iid, GraphQL::Types::String, null: false,
description: 'Internal ID of the pipeline.'
- field :sha, GraphQL::STRING_TYPE, null: false,
+ field :sha, GraphQL::Types::String, null: false,
description: "SHA of the pipeline's commit."
- field :before_sha, GraphQL::STRING_TYPE, null: true,
+ field :before_sha, GraphQL::Types::String, null: true,
description: 'Base SHA of the source branch.'
- field :complete, GraphQL::BOOLEAN_TYPE, null: false, method: :complete?,
+ field :complete, GraphQL::Types::Boolean, 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})"
- field :warnings, GraphQL::BOOLEAN_TYPE, null: false, method: :has_warnings?,
+ field :warnings, GraphQL::Types::Boolean, null: false, method: :has_warnings?,
description: "Indicates if a pipeline has warnings."
field :detailed_status, Types::Ci::DetailedStatusType, null: false,
@@ -39,7 +39,7 @@ module Types
field :config_source, PipelineConfigSourceEnum, null: true,
description: "Configuration source of the pipeline (#{::Enums::Ci::Pipeline.config_sources.keys.join(', ').upcase})"
- field :duration, GraphQL::INT_TYPE, null: true,
+ field :duration, GraphQL::Types::Int, null: true,
description: 'Duration of the pipeline in seconds.'
field :queued_duration, Types::DurationType, null: true,
@@ -76,12 +76,12 @@ module Types
null: true,
description: 'Pipeline user.'
- field :retryable, GraphQL::BOOLEAN_TYPE,
+ field :retryable, GraphQL::Types::Boolean,
description: 'Specifies if a pipeline can be retried.',
method: :retryable?,
null: false
- field :cancelable, GraphQL::BOOLEAN_TYPE,
+ field :cancelable, GraphQL::Types::Boolean,
description: 'Specifies if a pipeline can be canceled.',
method: :cancelable?,
null: false
@@ -103,7 +103,7 @@ module Types
required: false,
description: 'ID of the job.'
argument :name,
- type: ::GraphQL::STRING_TYPE,
+ type: ::GraphQL::Types::String,
required: false,
description: 'Name of the job.'
end
@@ -122,19 +122,19 @@ module Types
description: 'Pipeline that triggered the pipeline.',
method: :triggered_by_pipeline
- field :path, GraphQL::STRING_TYPE, null: true,
+ field :path, GraphQL::Types::String, null: true,
description: "Relative path to the pipeline's page."
- field :commit_path, GraphQL::STRING_TYPE, null: true,
+ field :commit_path, GraphQL::Types::String, null: true,
description: 'Path to the commit that triggered the pipeline.'
field :project, Types::ProjectType, null: true,
description: 'Project the pipeline belongs to.'
- field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?,
+ field :active, GraphQL::Types::Boolean, null: false, method: :active?,
description: 'Indicates if the pipeline is active.'
- field :uses_needs, GraphQL::BOOLEAN_TYPE, null: true,
+ field :uses_needs, GraphQL::Types::Boolean, null: true,
method: :uses_needs?,
description: 'Indicates if the pipeline has jobs with `needs` dependencies.'
@@ -150,7 +150,7 @@ module Types
description: 'A specific test suite in a pipeline test report.',
resolver: Resolvers::Ci::TestSuiteResolver
- field :ref, GraphQL::STRING_TYPE, null: true,
+ field :ref, GraphQL::Types::String, null: true,
description: 'Reference to the branch from which the pipeline was triggered.'
def detailed_status
diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb
index eeff7222762..f56b0939086 100644
--- a/app/graphql/types/ci/recent_failures_type.rb
+++ b/app/graphql/types/ci/recent_failures_type.rb
@@ -9,10 +9,10 @@ module Types
connection_type_class(Types::CountableConnectionType)
- field :count, GraphQL::INT_TYPE, null: true,
+ field :count, GraphQL::Types::Int, null: true,
description: 'Number of times the test case has failed in the past 14 days.'
- field :base_branch, GraphQL::STRING_TYPE, null: true,
+ field :base_branch, GraphQL::Types::String, null: true,
description: 'Name of the base branch of the project.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/ci/runner_architecture_type.rb b/app/graphql/types/ci/runner_architecture_type.rb
index 229974d4d13..08d3f98592b 100644
--- a/app/graphql/types/ci/runner_architecture_type.rb
+++ b/app/graphql/types/ci/runner_architecture_type.rb
@@ -6,9 +6,9 @@ module Types
class RunnerArchitectureType < BaseObject
graphql_name 'RunnerArchitecture'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name of the runner platform architecture.'
- field :download_location, GraphQL::STRING_TYPE, null: false,
+ field :download_location, GraphQL::Types::String, null: false,
description: 'Download location for the runner for the platform architecture.'
end
end
diff --git a/app/graphql/types/ci/runner_platform_type.rb b/app/graphql/types/ci/runner_platform_type.rb
index 5636f88835e..ffcf6364968 100644
--- a/app/graphql/types/ci/runner_platform_type.rb
+++ b/app/graphql/types/ci/runner_platform_type.rb
@@ -6,9 +6,9 @@ module Types
class RunnerPlatformType < BaseObject
graphql_name 'RunnerPlatform'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name slug of the runner platform.'
- field :human_readable_name, GraphQL::STRING_TYPE, null: false,
+ field :human_readable_name, GraphQL::Types::String, null: false,
description: 'Human readable name of the runner platform.'
field :architectures, Types::Ci::RunnerArchitectureType.connection_type, null: true,
description: 'Runner architectures supported for the platform.'
diff --git a/app/graphql/types/ci/runner_setup_type.rb b/app/graphql/types/ci/runner_setup_type.rb
index 61a2ea2a411..70f33cef8ad 100644
--- a/app/graphql/types/ci/runner_setup_type.rb
+++ b/app/graphql/types/ci/runner_setup_type.rb
@@ -6,9 +6,9 @@ module Types
class RunnerSetupType < BaseObject
graphql_name 'RunnerSetup'
- field :install_instructions, GraphQL::STRING_TYPE, null: false,
+ field :install_instructions, GraphQL::Types::String, null: false,
description: 'Instructions for installing the runner on the specified architecture.'
- field :register_instructions, GraphQL::STRING_TYPE, null: true,
+ field :register_instructions, GraphQL::Types::String, null: true,
description: 'Instructions for registering the runner.'
end
end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index 9c5041b0860..e2c8070af0c 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -5,6 +5,7 @@ module Types
class RunnerType < BaseObject
graphql_name 'CiRunner'
authorize :read_runner
+ present_using ::Ci::RunnerPresenter
JOB_COUNT_LIMIT = 1000
@@ -12,38 +13,38 @@ module Types
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
description: 'ID of the runner.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, 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,
+ field :maximum_timeout, GraphQL::Types::Int, 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,
+ field :active, GraphQL::Types::Boolean, 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: true,
+ field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.'
- field :short_sha, GraphQL::STRING_TYPE, null: true,
+ field :short_sha, GraphQL::Types::String, 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: true,
+ field :revision, GraphQL::Types::String, null: true,
description: 'Revision of the runner.'
- field :locked, GraphQL::BOOLEAN_TYPE, null: true,
+ field :locked, GraphQL::Types::Boolean, null: true,
description: 'Indicates the runner is locked.'
- field :run_untagged, GraphQL::BOOLEAN_TYPE, null: false,
+ field :run_untagged, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is able to run untagged jobs.'
- field :ip_address, GraphQL::STRING_TYPE, null: true,
+ field :ip_address, GraphQL::Types::String, null: true,
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,
+ field :tag_list, [GraphQL::Types::String], null: true,
description: 'Tags associated with the runner.'
- field :project_count, GraphQL::INT_TYPE, null: true,
+ field :project_count, GraphQL::Types::Int, null: true,
description: 'Number of projects that the runner is associated with.'
- field :job_count, GraphQL::INT_TYPE, null: true,
+ field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
def job_count
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
index ce3edb6c54f..63357e2345b 100644
--- a/app/graphql/types/ci/stage_type.rb
+++ b/app/graphql/types/ci/stage_type.rb
@@ -6,9 +6,9 @@ module Types
graphql_name 'CiStage'
authorize :read_commit_status
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the stage.'
- field :name, type: GraphQL::STRING_TYPE, null: true,
+ field :name, type: GraphQL::Types::String, null: true,
description: 'Name of the stage.'
field :groups, type: Ci::GroupType.connection_type, null: true,
extras: [:lookahead],
@@ -18,7 +18,7 @@ module Types
field :jobs, Ci::JobType.connection_type, null: true,
description: 'Jobs for the stage.',
method: 'latest_statuses'
- field :status, GraphQL::STRING_TYPE,
+ field :status, GraphQL::Types::String,
null: true,
description: 'Status of the pipeline stage.'
@@ -52,9 +52,13 @@ module Types
# rubocop: disable CodeReuse/ActiveRecord
def jobs_for_pipeline(pipeline, stage_ids, include_needs)
- results = pipeline.latest_statuses.where(stage_id: stage_ids)
- results = results.preload(:project)
- results = results.preload(:needs) if include_needs
+ builds_results = pipeline.latest_builds.where(stage_id: stage_ids).preload(:job_artifacts, :project)
+ bridges_results = pipeline.bridges.where(stage_id: stage_ids).preload(:project)
+ builds_results = builds_results.preload(:needs) if include_needs
+ bridges_results = bridges_results.preload(:needs) if include_needs
+ commit_status_results = pipeline.latest_statuses.where(stage_id: stage_ids)
+
+ results = builds_results | bridges_results | commit_status_results
results.group_by(&:stage_id)
end
diff --git a/app/graphql/types/ci/status_action_type.rb b/app/graphql/types/ci/status_action_type.rb
index a06b09735b3..15e5344e130 100644
--- a/app/graphql/types/ci/status_action_type.rb
+++ b/app/graphql/types/ci/status_action_type.rb
@@ -5,19 +5,19 @@ module Types
class StatusActionType < BaseObject
graphql_name 'StatusAction'
- field :id, GraphQL::STRING_TYPE, null: false,
+ field :id, GraphQL::Types::String, null: false,
description: 'ID for a status action.',
extras: [:parent]
- field :button_title, GraphQL::STRING_TYPE, null: true,
+ field :button_title, GraphQL::Types::String, null: true,
description: 'Title for the button, for example: Retry this job.'
- field :icon, GraphQL::STRING_TYPE, null: true,
+ field :icon, GraphQL::Types::String, null: true,
description: 'Icon used in the action button.'
- field :method, GraphQL::STRING_TYPE, null: true,
+ field :method, GraphQL::Types::String, null: true,
description: 'Method for the action, for example: :post.',
resolver_method: :action_method
- field :path, GraphQL::STRING_TYPE, null: true,
+ field :path, GraphQL::Types::String, null: true,
description: 'Path for the action.'
- field :title, GraphQL::STRING_TYPE, null: true,
+ field :title, GraphQL::Types::String, null: true,
description: 'Title for the action, for example: Retry.'
def id(parent:)
diff --git a/app/graphql/types/ci/template_type.rb b/app/graphql/types/ci/template_type.rb
index 5f07fa16928..7e7ee44025f 100644
--- a/app/graphql/types/ci/template_type.rb
+++ b/app/graphql/types/ci/template_type.rb
@@ -7,9 +7,9 @@ module Types
graphql_name 'CiTemplate'
description 'GitLab CI/CD configuration template.'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name of the CI template.'
- field :content, GraphQL::STRING_TYPE, null: false,
+ field :content, GraphQL::Types::String, null: false,
description: 'Contents of the CI template.'
end
end
diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb
index 9cc3f918125..9ec5daa44ea 100644
--- a/app/graphql/types/ci/test_case_type.rb
+++ b/app/graphql/types/ci/test_case_type.rb
@@ -12,25 +12,25 @@ module Types
field :status, Types::Ci::TestCaseStatusEnum, null: true,
description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})."
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the test case.'
- field :classname, GraphQL::STRING_TYPE, null: true,
+ field :classname, GraphQL::Types::String, null: true,
description: 'Classname of the test case.'
field :execution_time, GraphQL::FLOAT_TYPE, null: true,
description: 'Test case execution time in seconds.'
- field :file, GraphQL::STRING_TYPE, null: true,
+ field :file, GraphQL::Types::String, null: true,
description: 'Path to the file of the test case.'
- field :attachment_url, GraphQL::STRING_TYPE, null: true,
+ field :attachment_url, GraphQL::Types::String, null: true,
description: 'URL of the test case attachment file.'
- field :system_output, GraphQL::STRING_TYPE, null: true,
+ field :system_output, GraphQL::Types::String, null: true,
description: 'System output of the test case.'
- field :stack_trace, GraphQL::STRING_TYPE, null: true,
+ field :stack_trace, GraphQL::Types::String, null: true,
description: 'Stack trace of the test case.'
field :recent_failures, Types::Ci::RecentFailuresType, null: true,
diff --git a/app/graphql/types/ci/test_report_total_type.rb b/app/graphql/types/ci/test_report_total_type.rb
index 1123734adc3..aa07a391519 100644
--- a/app/graphql/types/ci/test_report_total_type.rb
+++ b/app/graphql/types/ci/test_report_total_type.rb
@@ -10,22 +10,22 @@ module Types
field :time, GraphQL::FLOAT_TYPE, null: true,
description: 'Total duration of the tests.'
- field :count, GraphQL::INT_TYPE, null: true,
+ field :count, GraphQL::Types::Int, null: true,
description: 'Total number of the test cases.'
- field :success, GraphQL::INT_TYPE, null: true,
+ field :success, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that succeeded.'
- field :failed, GraphQL::INT_TYPE, null: true,
+ field :failed, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that failed.'
- field :skipped, GraphQL::INT_TYPE, null: true,
+ field :skipped, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that were skipped.'
- field :error, GraphQL::INT_TYPE, null: true,
+ field :error, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that had an error.'
- field :suite_error, GraphQL::STRING_TYPE, null: true,
+ field :suite_error, GraphQL::Types::String, null: true,
description: 'Test suite error message.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/ci/test_suite_summary_type.rb b/app/graphql/types/ci/test_suite_summary_type.rb
index a80a9179cb4..3db2d80d591 100644
--- a/app/graphql/types/ci/test_suite_summary_type.rb
+++ b/app/graphql/types/ci/test_suite_summary_type.rb
@@ -9,31 +9,31 @@ module Types
connection_type_class(Types::CountableConnectionType)
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the test suite.'
field :total_time, GraphQL::FLOAT_TYPE, null: true,
description: 'Total duration of the tests in the test suite.'
- field :total_count, GraphQL::INT_TYPE, null: true,
+ field :total_count, GraphQL::Types::Int, null: true,
description: 'Total number of the test cases in the test suite.'
- field :success_count, GraphQL::INT_TYPE, null: true,
+ field :success_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that succeeded in the test suite.'
- field :failed_count, GraphQL::INT_TYPE, null: true,
+ field :failed_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that failed in the test suite.'
- field :skipped_count, GraphQL::INT_TYPE, null: true,
+ field :skipped_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that were skipped in the test suite.'
- field :error_count, GraphQL::INT_TYPE, null: true,
+ field :error_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that had an error.'
- field :suite_error, GraphQL::STRING_TYPE, null: true,
+ field :suite_error, GraphQL::Types::String, null: true,
description: 'Test suite error message.'
- field :build_ids, [GraphQL::ID_TYPE], null: true,
+ field :build_ids, [GraphQL::Types::ID], null: true,
description: 'IDs of the builds used to run the test suite.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb
index 7d4c01da81b..f9f37d4045e 100644
--- a/app/graphql/types/ci/test_suite_type.rb
+++ b/app/graphql/types/ci/test_suite_type.rb
@@ -9,28 +9,28 @@ module Types
connection_type_class(Types::CountableConnectionType)
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the test suite.'
field :total_time, GraphQL::FLOAT_TYPE, null: true,
description: 'Total duration of the tests in the test suite.'
- field :total_count, GraphQL::INT_TYPE, null: true,
+ field :total_count, GraphQL::Types::Int, null: true,
description: 'Total number of the test cases in the test suite.'
- field :success_count, GraphQL::INT_TYPE, null: true,
+ field :success_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that succeeded in the test suite.'
- field :failed_count, GraphQL::INT_TYPE, null: true,
+ field :failed_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that failed in the test suite.'
- field :skipped_count, GraphQL::INT_TYPE, null: true,
+ field :skipped_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that were skipped in the test suite.'
- field :error_count, GraphQL::INT_TYPE, null: true,
+ field :error_count, GraphQL::Types::Int, null: true,
description: 'Total number of test cases that had an error.'
- field :suite_error, GraphQL::STRING_TYPE, null: true,
+ field :suite_error, GraphQL::Types::String, null: true,
description: 'Test suite error message.'
field :test_cases, Types::Ci::TestCaseType.connection_type, null: true,
diff --git a/app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb b/app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb
index ccb72283cf6..68b9a63d8dc 100644
--- a/app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb
+++ b/app/graphql/types/ci_configuration/sast/analyzers_entity_input_type.rb
@@ -7,10 +7,10 @@ module Types
graphql_name 'SastCiConfigurationAnalyzersEntityInput'
description 'Represents the analyzers entity in SAST CI configuration'
- argument :name, GraphQL::STRING_TYPE, required: true,
+ argument :name, GraphQL::Types::String, required: true,
description: 'Name of analyzer.'
- argument :enabled, GraphQL::BOOLEAN_TYPE, required: true,
+ argument :enabled, GraphQL::Types::Boolean, required: true,
description: 'State of the analyzer.'
argument :variables, [::Types::CiConfiguration::Sast::EntityInputType],
diff --git a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb b/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb
index 3c6202ca7e0..9fdc7c1b000 100644
--- a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb
+++ b/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb
@@ -8,16 +8,16 @@ module Types
graphql_name 'SastCiConfigurationAnalyzersEntity'
description 'Represents an analyzer entity in SAST CI configuration'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the analyzer.'
- field :label, GraphQL::STRING_TYPE, null: true,
+ field :label, GraphQL::Types::String, null: true,
description: 'Analyzer label used in the config UI.'
- field :enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates whether an analyzer is enabled.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Analyzer description that is displayed on the form.'
field :variables, ::Types::CiConfiguration::Sast::EntityType.connection_type, null: true,
diff --git a/app/graphql/types/ci_configuration/sast/entity_input_type.rb b/app/graphql/types/ci_configuration/sast/entity_input_type.rb
index 9fce120889b..f0e3c07d71f 100644
--- a/app/graphql/types/ci_configuration/sast/entity_input_type.rb
+++ b/app/graphql/types/ci_configuration/sast/entity_input_type.rb
@@ -7,13 +7,13 @@ module Types
graphql_name 'SastCiConfigurationEntityInput'
description 'Represents an entity in SAST CI configuration'
- argument :field, GraphQL::STRING_TYPE, required: true,
+ argument :field, GraphQL::Types::String, required: true,
description: 'CI keyword of entity.'
- argument :default_value, GraphQL::STRING_TYPE, required: true,
+ argument :default_value, GraphQL::Types::String, required: true,
description: 'Default value that is used if value is empty.'
- argument :value, GraphQL::STRING_TYPE, required: true,
+ argument :value, GraphQL::Types::String, required: true,
description: 'Current value of the entity.'
end
end
diff --git a/app/graphql/types/ci_configuration/sast/entity_type.rb b/app/graphql/types/ci_configuration/sast/entity_type.rb
index eeb9025391f..41b8575d99a 100644
--- a/app/graphql/types/ci_configuration/sast/entity_type.rb
+++ b/app/graphql/types/ci_configuration/sast/entity_type.rb
@@ -8,25 +8,25 @@ module Types
graphql_name 'SastCiConfigurationEntity'
description 'Represents an entity in SAST CI configuration'
- field :field, GraphQL::STRING_TYPE, null: true,
+ field :field, GraphQL::Types::String, null: true,
description: 'CI keyword of entity.'
- field :label, GraphQL::STRING_TYPE, null: true,
+ field :label, GraphQL::Types::String, null: true,
description: 'Label for entity used in the form.'
- field :type, GraphQL::STRING_TYPE, null: true,
+ field :type, GraphQL::Types::String, null: true,
description: 'Type of the field value.'
field :options, ::Types::CiConfiguration::Sast::OptionsEntityType.connection_type, null: true,
description: 'Different possible values of the field.'
- field :default_value, GraphQL::STRING_TYPE, null: true,
+ field :default_value, GraphQL::Types::String, null: true,
description: 'Default value that is used if value is empty.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Entity description that is displayed on the form.'
- field :value, GraphQL::STRING_TYPE, null: true,
+ field :value, GraphQL::Types::String, null: true,
description: 'Current value of the entity.'
field :size, ::Types::CiConfiguration::Sast::UiComponentSizeEnum, null: true,
diff --git a/app/graphql/types/ci_configuration/sast/options_entity_type.rb b/app/graphql/types/ci_configuration/sast/options_entity_type.rb
index 86d104a7fda..5f365807cfe 100644
--- a/app/graphql/types/ci_configuration/sast/options_entity_type.rb
+++ b/app/graphql/types/ci_configuration/sast/options_entity_type.rb
@@ -8,10 +8,10 @@ module Types
graphql_name 'SastCiConfigurationOptionsEntity'
description 'Represents an entity for options in SAST CI configuration'
- field :label, GraphQL::STRING_TYPE, null: true,
+ field :label, GraphQL::Types::String, null: true,
description: 'Label of option entity.'
- field :value, GraphQL::STRING_TYPE, null: true,
+ field :value, GraphQL::Types::String, null: true,
description: 'Value of option entity.'
end
end
diff --git a/app/graphql/types/commit_action_type.rb b/app/graphql/types/commit_action_type.rb
index cc1f45478e4..b170134b388 100644
--- a/app/graphql/types/commit_action_type.rb
+++ b/app/graphql/types/commit_action_type.rb
@@ -4,15 +4,15 @@ module Types
class CommitActionType < BaseInputObject
argument :action, type: Types::CommitActionModeEnum, required: true,
description: 'The action to perform, create, delete, move, update, chmod.'
- argument :file_path, type: GraphQL::STRING_TYPE, required: true,
+ argument :file_path, type: GraphQL::Types::String, required: true,
description: 'Full path to the file.'
- argument :content, type: GraphQL::STRING_TYPE, required: false,
+ argument :content, type: GraphQL::Types::String, required: false,
description: 'Content of the file.'
- argument :previous_path, type: GraphQL::STRING_TYPE, required: false,
+ argument :previous_path, type: GraphQL::Types::String, required: false,
description: 'Original full path to the file being moved.'
- argument :last_commit_id, type: GraphQL::STRING_TYPE, required: false,
+ argument :last_commit_id, type: GraphQL::Types::String, required: false,
description: 'Last known file commit ID.'
- argument :execute_filemode, type: GraphQL::BOOLEAN_TYPE, required: false,
+ argument :execute_filemode, type: GraphQL::Types::Boolean, required: false,
description: 'Enables/disables the execute flag on the file.'
argument :encoding, type: Types::CommitEncodingEnum, required: false,
description: 'Encoding of the file. Default is text.'
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index d137901380b..dc1bf6a3101 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -8,31 +8,31 @@ module Types
present_using CommitPresenter
- field :id, type: GraphQL::ID_TYPE, null: false,
+ field :id, type: GraphQL::Types::ID, null: false,
description: 'ID (global ID) of the commit.'
- field :sha, type: GraphQL::STRING_TYPE, null: false,
+ field :sha, type: GraphQL::Types::String, null: false,
description: 'SHA1 ID of the commit.'
- field :short_id, type: GraphQL::STRING_TYPE, null: false,
+ field :short_id, type: GraphQL::Types::String, null: false,
description: 'Short SHA1 ID of the commit.'
- field :title, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ field :title, type: GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Title of the commit message.'
markdown_field :title_html, null: true
- field :description, type: GraphQL::STRING_TYPE, null: true,
+ field :description, type: GraphQL::Types::String, null: true,
description: 'Description of the commit message.'
markdown_field :description_html, null: true
- field :message, type: GraphQL::STRING_TYPE, null: true,
+ field :message, type: GraphQL::Types::String, null: true,
description: 'Raw commit message.'
field :authored_date, type: Types::TimeType, null: true,
description: 'Timestamp of when the commit was authored.'
- field :web_url, type: GraphQL::STRING_TYPE, null: false,
+ field :web_url, type: GraphQL::Types::String, null: false,
description: 'Web URL of the commit.'
- field :web_path, type: GraphQL::STRING_TYPE, null: false,
+ field :web_path, type: GraphQL::Types::String, null: false,
description: 'Web path of the commit.'
- field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ field :signature_html, type: GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Rendered HTML of the commit signature.'
- field :author_name, type: GraphQL::STRING_TYPE, null: true,
+ field :author_name, type: GraphQL::Types::String, null: true,
description: 'Commit authors name.'
- field :author_gravatar, type: GraphQL::STRING_TYPE, null: true,
+ field :author_gravatar, type: GraphQL::Types::String, null: true,
description: 'Commit authors gravatar.'
# models/commit lazy loads the author by email
diff --git a/app/graphql/types/container_expiration_policy_type.rb b/app/graphql/types/container_expiration_policy_type.rb
index 2b01474617a..6d6df21fe3f 100644
--- a/app/graphql/types/container_expiration_policy_type.rb
+++ b/app/graphql/types/container_expiration_policy_type.rb
@@ -10,7 +10,7 @@ module Types
field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was created.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the container expiration policy was updated.'
- field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether this container expiration policy is enabled.'
+ field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether this container expiration policy is enabled.'
field :older_than, Types::ContainerExpirationPolicyOlderThanEnum, null: true, description: 'Tags older that this will expire.'
field :cadence, Types::ContainerExpirationPolicyCadenceEnum, null: false, description: 'This container expiration policy schedule.'
field :keep_n, Types::ContainerExpirationPolicyKeepEnum, null: true, description: 'Number of tags to retain.'
diff --git a/app/graphql/types/container_repository_tag_type.rb b/app/graphql/types/container_repository_tag_type.rb
index 6de16416395..b6b65bce421 100644
--- a/app/graphql/types/container_repository_tag_type.rb
+++ b/app/graphql/types/container_repository_tag_type.rb
@@ -8,15 +8,15 @@ module Types
authorize :read_container_image
- field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the tag.'
- field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the tag.'
- field :location, GraphQL::STRING_TYPE, null: false, description: 'URL of the tag.'
- field :digest, GraphQL::STRING_TYPE, null: true, description: 'Digest of the tag.'
- field :revision, GraphQL::STRING_TYPE, null: true, description: 'Revision of the tag.'
- field :short_revision, GraphQL::STRING_TYPE, null: true, description: 'Short revision of the tag.'
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the tag.'
+ field :path, GraphQL::Types::String, null: false, description: 'Path of the tag.'
+ field :location, GraphQL::Types::String, null: false, description: 'URL of the tag.'
+ field :digest, GraphQL::Types::String, null: true, description: 'Digest of the tag.'
+ field :revision, GraphQL::Types::String, null: true, description: 'Revision of the tag.'
+ field :short_revision, GraphQL::Types::String, null: true, description: 'Short revision of the tag.'
field :total_size, GraphQL::Types::BigInt, null: true, description: 'The size of the tag.'
field :created_at, Types::TimeType, null: true, description: 'Timestamp when the tag was created.'
- field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete this tag.'
+ field :can_delete, GraphQL::Types::Boolean, null: false, description: 'Can the current user delete this tag.'
def can_delete
Ability.allowed?(current_user, :destroy_container_image, object)
diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb
index 48c2b9f460f..91a65053131 100644
--- a/app/graphql/types/container_repository_type.rb
+++ b/app/graphql/types/container_repository_type.rb
@@ -8,17 +8,17 @@ module Types
authorize :read_container_image
- field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the container repository.'
- field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the container repository.'
- field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the container repository.'
- field :location, GraphQL::STRING_TYPE, null: false, description: 'URL of the container repository.'
+ field :id, GraphQL::Types::ID, null: false, description: 'ID of the container repository.'
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the container repository.'
+ field :path, GraphQL::Types::String, null: false, description: 'Path of the container repository.'
+ field :location, GraphQL::Types::String, null: false, description: 'URL of the container repository.'
field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.'
field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'The tags cleanup status for the container repository.'
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
- field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.'
- field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.'
+ field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.'
+ field :can_delete, GraphQL::Types::Boolean, null: false, description: 'Can the current user delete the container repository.'
field :project, Types::ProjectType, null: false, description: 'Project of the container registry.'
def can_delete
diff --git a/app/graphql/types/countable_connection_type.rb b/app/graphql/types/countable_connection_type.rb
index 0a9699a4570..0f24964daa6 100644
--- a/app/graphql/types/countable_connection_type.rb
+++ b/app/graphql/types/countable_connection_type.rb
@@ -3,7 +3,7 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
class CountableConnectionType < GraphQL::Types::Relay::BaseConnection
- field :count, GraphQL::INT_TYPE, null: false,
+ field :count, GraphQL::Types::Int, null: false,
description: 'Total count of collection.'
def count
diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb
index 246b60ce184..64381b3ee1e 100644
--- a/app/graphql/types/custom_emoji_type.rb
+++ b/app/graphql/types/custom_emoji_type.rb
@@ -11,16 +11,16 @@ module Types
null: false,
description: 'The ID of the emoji.'
- field :name, GraphQL::STRING_TYPE,
+ field :name, GraphQL::Types::String,
null: false,
description: 'The name of the emoji.'
- field :url, GraphQL::STRING_TYPE,
+ field :url, GraphQL::Types::String,
null: false,
method: :file,
description: 'The link to file of the emoji.'
- field :external, GraphQL::BOOLEAN_TYPE,
+ field :external, GraphQL::Types::Boolean,
null: false,
description: 'Whether the emoji is an external link.'
end
diff --git a/app/graphql/types/design_management/design_fields.rb b/app/graphql/types/design_management/design_fields.rb
index b770e30f5be..7779c3f1bcb 100644
--- a/app/graphql/types/design_management/design_fields.rb
+++ b/app/graphql/types/design_management/design_fields.rb
@@ -7,13 +7,13 @@ module Types
field_class Types::BaseField
- field :id, GraphQL::ID_TYPE, description: 'The ID of this design.', null: false
+ field :id, GraphQL::Types::ID, description: 'The ID of this design.', null: false
field :project, Types::ProjectType, null: false, description: 'The project the design belongs to.'
field :issue, Types::IssueType, null: false, description: 'The issue the design belongs to.'
- field :filename, GraphQL::STRING_TYPE, null: false, description: 'The filename of the design.'
- field :full_path, GraphQL::STRING_TYPE, null: false, description: 'The full path to the design file.'
- field :image, GraphQL::STRING_TYPE, null: false, extras: [:parent], description: 'The URL of the full-sized image.'
- field :image_v432x230, GraphQL::STRING_TYPE, null: true, extras: [:parent],
+ field :filename, GraphQL::Types::String, null: false, description: 'The filename of the design.'
+ field :full_path, GraphQL::Types::String, null: false, description: 'The full path to the design file.'
+ field :image, GraphQL::Types::String, null: false, extras: [:parent], description: 'The URL of the full-sized image.'
+ field :image_v432x230, GraphQL::Types::String, null: true, extras: [:parent],
description: 'The URL of the design resized to fit within the bounds of 432x230. ' \
'This will be `null` if the image has not been generated'
field :diff_refs, Types::DiffRefsType,
@@ -26,7 +26,7 @@ module Types
extras: [:parent],
description: 'How this design was changed in the current version.'
field :notes_count,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: false,
method: :user_notes_count,
description: 'The total count of user-created notes for this design.'
diff --git a/app/graphql/types/design_management/version_type.rb b/app/graphql/types/design_management/version_type.rb
index 265d6185110..cfd2b887dc3 100644
--- a/app/graphql/types/design_management/version_type.rb
+++ b/app/graphql/types/design_management/version_type.rb
@@ -11,9 +11,9 @@ module Types
authorize :read_design
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the design version.'
- field :sha, GraphQL::ID_TYPE, null: false,
+ field :sha, GraphQL::Types::ID, null: false,
description: 'SHA of the design version.'
field :designs,
diff --git a/app/graphql/types/diff_paths_input_type.rb b/app/graphql/types/diff_paths_input_type.rb
index d148b7656eb..e1d3d58411c 100644
--- a/app/graphql/types/diff_paths_input_type.rb
+++ b/app/graphql/types/diff_paths_input_type.rb
@@ -2,9 +2,9 @@
module Types
class DiffPathsInputType < BaseInputObject
- argument :old_path, GraphQL::STRING_TYPE, required: false,
+ argument :old_path, GraphQL::Types::String, required: false,
description: 'The path of the file on the start sha.'
- argument :new_path, GraphQL::STRING_TYPE, required: false,
+ argument :new_path, GraphQL::Types::String, required: false,
description: 'The path of the file on the head sha.'
end
end
diff --git a/app/graphql/types/diff_refs_type.rb b/app/graphql/types/diff_refs_type.rb
index 3c8f934f1eb..b19d09c789c 100644
--- a/app/graphql/types/diff_refs_type.rb
+++ b/app/graphql/types/diff_refs_type.rb
@@ -6,11 +6,11 @@ module Types
class DiffRefsType < BaseObject
graphql_name 'DiffRefs'
- field :head_sha, GraphQL::STRING_TYPE, null: false,
+ field :head_sha, GraphQL::Types::String, null: false,
description: 'SHA of the HEAD at the time the comment was made.'
- field :base_sha, GraphQL::STRING_TYPE, null: true,
+ field :base_sha, GraphQL::Types::String, null: true,
description: 'Merge base of the branch the comment was made on.'
- field :start_sha, GraphQL::STRING_TYPE, null: false,
+ field :start_sha, GraphQL::Types::String, null: false,
description: 'SHA of the branch being compared against.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/diff_stats_summary_type.rb b/app/graphql/types/diff_stats_summary_type.rb
index 78c0e2f2b4c..079c73d0759 100644
--- a/app/graphql/types/diff_stats_summary_type.rb
+++ b/app/graphql/types/diff_stats_summary_type.rb
@@ -8,13 +8,13 @@ module Types
description 'Aggregated summary of changes'
- field :additions, GraphQL::INT_TYPE, null: false,
+ field :additions, GraphQL::Types::Int, null: false,
description: 'Number of lines added.'
- field :deletions, GraphQL::INT_TYPE, null: false,
+ field :deletions, GraphQL::Types::Int, null: false,
description: 'Number of lines deleted.'
- field :changes, GraphQL::INT_TYPE, null: false,
+ field :changes, GraphQL::Types::Int, null: false,
description: 'Number of lines changed.'
- field :file_count, GraphQL::INT_TYPE, null: false,
+ field :file_count, GraphQL::Types::Int, null: false,
description: 'Number of files changed.'
def changes
diff --git a/app/graphql/types/diff_stats_type.rb b/app/graphql/types/diff_stats_type.rb
index 8a6840e5a94..60aacca8ce5 100644
--- a/app/graphql/types/diff_stats_type.rb
+++ b/app/graphql/types/diff_stats_type.rb
@@ -8,11 +8,11 @@ module Types
description 'Changes to a single file'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'File path, relative to repository root.'
- field :additions, GraphQL::INT_TYPE, null: false,
+ field :additions, GraphQL::Types::Int, null: false,
description: 'Number of lines added to this file.'
- field :deletions, GraphQL::INT_TYPE, null: false,
+ field :deletions, GraphQL::Types::Int, null: false,
description: 'Number of lines deleted from this file.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 2e6417f08ea..267ca944198 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -9,16 +9,16 @@ module Types
authorize :read_environment
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Human-readable name of the environment.'
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the environment.'
- field :state, GraphQL::STRING_TYPE, null: false,
+ field :state, GraphQL::Types::String, null: false,
description: 'State of the environment, for example: available/stopped.'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'The path to the environment.'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
index 59bd97e3448..79e789d3f8b 100644
--- a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
@@ -10,23 +10,23 @@ module Types
authorize :read_sentry_issue
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
null: false,
description: 'ID (global ID) of the error.'
- field :sentry_id, GraphQL::STRING_TYPE,
+ field :sentry_id, GraphQL::Types::String,
method: :id,
null: false,
description: 'ID (Sentry ID) of the error.'
- field :title, GraphQL::STRING_TYPE,
+ field :title, GraphQL::Types::String,
null: false,
description: 'Title of the error.'
- field :type, GraphQL::STRING_TYPE,
+ field :type, GraphQL::Types::String,
null: false,
description: 'Type of the error.'
- field :user_count, GraphQL::INT_TYPE,
+ field :user_count, GraphQL::Types::Int,
null: false,
description: 'Count of users affected by the error.'
- field :count, GraphQL::INT_TYPE,
+ field :count, GraphQL::Types::Int,
null: false,
description: 'Count of occurrences.'
field :first_seen, Types::TimeType,
@@ -35,31 +35,31 @@ module Types
field :last_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was last seen.'
- field :message, GraphQL::STRING_TYPE,
+ field :message, GraphQL::Types::String,
null: true,
description: 'Sentry metadata message of the error.'
- field :culprit, GraphQL::STRING_TYPE,
+ field :culprit, GraphQL::Types::String,
null: false,
description: 'Culprit of the error.'
- field :external_base_url, GraphQL::STRING_TYPE,
+ field :external_base_url, GraphQL::Types::String,
null: false,
description: 'External Base URL of the Sentry Instance.'
- field :external_url, GraphQL::STRING_TYPE,
+ field :external_url, GraphQL::Types::String,
null: false,
description: 'External URL of the error.'
- field :sentry_project_id, GraphQL::ID_TYPE,
+ field :sentry_project_id, GraphQL::Types::ID,
method: :project_id,
null: false,
description: 'ID of the project (Sentry project).'
- field :sentry_project_name, GraphQL::STRING_TYPE,
+ field :sentry_project_name, GraphQL::Types::String,
method: :project_name,
null: false,
description: 'Name of the project affected by the error.'
- field :sentry_project_slug, GraphQL::STRING_TYPE,
+ field :sentry_project_slug, GraphQL::Types::String,
method: :project_slug,
null: false,
description: 'Slug of the project affected by the error.'
- field :short_id, GraphQL::STRING_TYPE,
+ field :short_id, GraphQL::Types::String,
null: false,
description: 'Short ID (Sentry ID) of the error.'
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
@@ -68,31 +68,31 @@ module Types
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: 'Last 24hr stats of the error.'
- field :first_release_last_commit, GraphQL::STRING_TYPE,
+ field :first_release_last_commit, GraphQL::Types::String,
null: true,
description: 'Commit the error was first seen.'
- field :last_release_last_commit, GraphQL::STRING_TYPE,
+ field :last_release_last_commit, GraphQL::Types::String,
null: true,
description: 'Commit the error was last seen.'
- field :first_release_short_version, GraphQL::STRING_TYPE,
+ field :first_release_short_version, GraphQL::Types::String,
null: true,
description: 'Release short version the error was first seen.'
- field :last_release_short_version, GraphQL::STRING_TYPE,
+ field :last_release_short_version, GraphQL::Types::String,
null: true,
description: 'Release short version the error was last seen.'
- field :first_release_version, GraphQL::STRING_TYPE,
+ field :first_release_version, GraphQL::Types::String,
null: true,
description: 'Release version the error was first seen.'
- field :last_release_version, GraphQL::STRING_TYPE,
+ field :last_release_version, GraphQL::Types::String,
null: true,
description: 'Release version the error was last seen.'
- field :gitlab_commit, GraphQL::STRING_TYPE,
+ field :gitlab_commit, GraphQL::Types::String,
null: true,
description: 'GitLab commit SHA attributed to the Error based on the release version.'
- field :gitlab_commit_path, GraphQL::STRING_TYPE,
+ field :gitlab_commit_path, GraphQL::Types::String,
null: true,
description: 'Path to the GitLab page for the GitLab commit attributed to the error.'
- field :gitlab_issue_path, GraphQL::STRING_TYPE,
+ field :gitlab_issue_path, GraphQL::Types::String,
method: :gitlab_issue,
null: true,
description: 'URL of GitLab Issue.'
diff --git a/app/graphql/types/error_tracking/sentry_error_collection_type.rb b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
index d3941b7c410..2d8c3d3d326 100644
--- a/app/graphql/types/error_tracking/sentry_error_collection_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
@@ -18,7 +18,7 @@ module Types
description: 'Stack Trace of Sentry Error.',
resolver: Resolvers::ErrorTracking::SentryErrorStackTraceResolver
field :external_url,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: true,
description: "External URL for Sentry."
end
diff --git a/app/graphql/types/error_tracking/sentry_error_frequency_type.rb b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb
index 05af1391af3..49a1b1e0476 100644
--- a/app/graphql/types/error_tracking/sentry_error_frequency_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_frequency_type.rb
@@ -9,7 +9,7 @@ module Types
field :time, Types::TimeType,
null: false,
description: "Time the error frequency stats were recorded."
- field :count, GraphQL::INT_TYPE,
+ field :count, GraphQL::Types::Int,
null: false,
description: "Count of errors received since the previously recorded time."
end
diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb
index 0b3c4cf55b9..ad31854b30c 100644
--- a/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_context_type.rb
@@ -8,11 +8,11 @@ module Types
description 'An object context for a Sentry error stack trace'
field :line,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: false,
description: 'Line number of the context.'
field :code,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'Code number of the context.'
diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
index c9915d052f9..e8f78004569 100644
--- a/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_entry_type.rb
@@ -7,16 +7,16 @@ module Types
graphql_name 'SentryErrorStackTraceEntry'
description 'An object containing a stack trace entry for a Sentry error'
- field :function, GraphQL::STRING_TYPE,
+ field :function, GraphQL::Types::String,
null: true,
description: 'Function in which the Sentry error occurred.'
- field :col, GraphQL::STRING_TYPE,
+ field :col, GraphQL::Types::String,
null: true,
description: 'Function in which the Sentry error occurred.'
- field :line, GraphQL::STRING_TYPE,
+ field :line, GraphQL::Types::String,
null: true,
description: 'Function in which the Sentry error occurred.'
- field :file_name, GraphQL::STRING_TYPE,
+ field :file_name, GraphQL::Types::String,
null: true,
description: 'File in which the Sentry error occurred.'
field :trace_context, [Types::ErrorTracking::SentryErrorStackTraceContextType],
diff --git a/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
index 52959a9329b..dff52d77109 100644
--- a/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_stack_trace_type.rb
@@ -8,10 +8,10 @@ module Types
authorize :read_sentry_issue
- field :issue_id, GraphQL::STRING_TYPE,
+ field :issue_id, GraphQL::Types::String,
null: false,
description: 'ID of the Sentry error.'
- field :date_received, GraphQL::STRING_TYPE,
+ field :date_received, GraphQL::Types::String,
null: false,
description: 'Time the stack trace was received by Sentry.'
field :stack_trace_entries, [Types::ErrorTracking::SentryErrorStackTraceEntryType],
diff --git a/app/graphql/types/error_tracking/sentry_error_tags_type.rb b/app/graphql/types/error_tracking/sentry_error_tags_type.rb
index e2b051998c5..4d069615e3a 100644
--- a/app/graphql/types/error_tracking/sentry_error_tags_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_tags_type.rb
@@ -7,10 +7,10 @@ module Types
graphql_name 'SentryErrorTags'
description 'State of a Sentry error'
- field :level, GraphQL::STRING_TYPE,
+ field :level, GraphQL::Types::String,
null: true,
description: "Severity level of the Sentry Error."
- field :logger, GraphQL::STRING_TYPE,
+ field :logger, GraphQL::Types::String,
null: true,
description: "Logger of the Sentry Error."
end
diff --git a/app/graphql/types/error_tracking/sentry_error_type.rb b/app/graphql/types/error_tracking/sentry_error_type.rb
index c0e09fb8c65..aaa6cbfb28f 100644
--- a/app/graphql/types/error_tracking/sentry_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_type.rb
@@ -9,10 +9,10 @@ module Types
present_using SentryErrorPresenter
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
null: false,
description: 'ID (global ID) of the error.'
- field :sentry_id, GraphQL::STRING_TYPE,
+ field :sentry_id, GraphQL::Types::String,
method: :id,
null: false,
description: 'ID (Sentry ID) of the error.'
@@ -22,28 +22,28 @@ module Types
field :last_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was last seen.'
- field :title, GraphQL::STRING_TYPE,
+ field :title, GraphQL::Types::String,
null: false,
description: 'Title of the error.'
- field :type, GraphQL::STRING_TYPE,
+ field :type, GraphQL::Types::String,
null: false,
description: 'Type of the error.'
- field :user_count, GraphQL::INT_TYPE,
+ field :user_count, GraphQL::Types::Int,
null: false,
description: 'Count of users affected by the error.'
- field :count, GraphQL::INT_TYPE,
+ field :count, GraphQL::Types::Int,
null: false,
description: 'Count of occurrences.'
- field :message, GraphQL::STRING_TYPE,
+ field :message, GraphQL::Types::String,
null: true,
description: 'Sentry metadata message of the error.'
- field :culprit, GraphQL::STRING_TYPE,
+ field :culprit, GraphQL::Types::String,
null: false,
description: 'Culprit of the error.'
- field :external_url, GraphQL::STRING_TYPE,
+ field :external_url, GraphQL::Types::String,
null: false,
description: 'External URL of the error.'
- field :short_id, GraphQL::STRING_TYPE,
+ field :short_id, GraphQL::Types::String,
null: false,
description: 'Short ID (Sentry ID) of the error.'
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
@@ -52,15 +52,15 @@ module Types
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: 'Last 24hr stats of the error.'
- field :sentry_project_id, GraphQL::ID_TYPE,
+ field :sentry_project_id, GraphQL::Types::ID,
method: :project_id,
null: false,
description: 'ID of the project (Sentry project).'
- field :sentry_project_name, GraphQL::STRING_TYPE,
+ field :sentry_project_name, GraphQL::Types::String,
method: :project_name,
null: false,
description: 'Name of the project affected by the error.'
- field :sentry_project_slug, GraphQL::STRING_TYPE,
+ field :sentry_project_slug, GraphQL::Types::String,
method: :project_slug,
null: false,
description: 'Slug of the project affected by the error.'
diff --git a/app/graphql/types/event_type.rb b/app/graphql/types/event_type.rb
index 2a4c2e7c60a..f4e680e93ef 100644
--- a/app/graphql/types/event_type.rb
+++ b/app/graphql/types/event_type.rb
@@ -9,7 +9,7 @@ module Types
authorize :read_event
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
description: 'ID of the event.',
null: false
diff --git a/app/graphql/types/evidence_type.rb b/app/graphql/types/evidence_type.rb
index 6e56ad7d407..26fb64d25d8 100644
--- a/app/graphql/types/evidence_type.rb
+++ b/app/graphql/types/evidence_type.rb
@@ -9,11 +9,11 @@ module Types
present_using Releases::EvidencePresenter
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the evidence.'
- field :sha, GraphQL::STRING_TYPE, null: true,
+ field :sha, GraphQL::Types::String, null: true,
description: 'SHA1 ID of the evidence hash.'
- field :filepath, GraphQL::STRING_TYPE, null: true,
+ field :filepath, GraphQL::Types::String, null: true,
description: 'URL from where the evidence can be downloaded.'
field :collected_at, Types::TimeType, null: true,
description: 'Timestamp when the evidence was collected.'
diff --git a/app/graphql/types/grafana_integration_type.rb b/app/graphql/types/grafana_integration_type.rb
index 630d3a10e36..26fefd51e08 100644
--- a/app/graphql/types/grafana_integration_type.rb
+++ b/app/graphql/types/grafana_integration_type.rb
@@ -6,11 +6,11 @@ module Types
authorize :admin_operations
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'Internal ID of the Grafana integration.'
- field :grafana_url, GraphQL::STRING_TYPE, null: false,
+ field :grafana_url, GraphQL::Types::String, null: false,
description: 'URL for the Grafana host for the Grafana integration.'
- field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
+ field :enabled, GraphQL::Types::Boolean, null: false,
description: 'Indicates whether Grafana integration is enabled.'
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of the issue\'s creation.'
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 27f4ae47c41..fbf0084cd0e 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -9,12 +9,12 @@ module Types
expose_permissions Types::PermissionTypes::Group
field :web_url,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: false,
description: 'Web URL of the group.'
field :avatar_url,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
description: 'Avatar URL of the group.'
@@ -25,42 +25,42 @@ module Types
feature_flag: :custom_emoji
field :share_with_group_lock,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if sharing a project with another group within this group is prevented.'
field :project_creation_level,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
method: :project_creation_level_str,
description: 'The permission level required to create projects in the group.'
field :subgroup_creation_level,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
method: :subgroup_creation_level_str,
description: 'The permission level required to create subgroups within the group.'
field :require_two_factor_authentication,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if all users in this group are required to set up two-factor authentication.'
field :two_factor_grace_period,
- type: GraphQL::INT_TYPE,
+ type: GraphQL::Types::Int,
null: true,
description: 'Time before two-factor authentication is enforced.'
field :auto_devops_enabled,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates whether Auto DevOps is enabled for all projects within this group.'
field :emails_disabled,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if a group has email notifications disabled.'
field :mentions_disabled,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: true,
description: 'Indicates if a group is disabled from getting mentioned.'
@@ -103,7 +103,7 @@ module Types
null: true,
description: 'A label available on this group.' do
argument :title,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
required: true,
description: 'Title of the label.'
end
@@ -120,7 +120,7 @@ module Types
authorize: :read_container_image
field :container_repositories_count,
- type: GraphQL::INT_TYPE,
+ type: GraphQL::Types::Int,
null: false,
description: 'Number of container repositories in the group.'
@@ -149,6 +149,12 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
+ field :descendant_groups, Types::GroupType.connection_type,
+ null: true,
+ description: 'List of descendant groups of this group.',
+ complexity: 5,
+ resolver: Resolvers::GroupsResolver
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/invitation_interface.rb b/app/graphql/types/invitation_interface.rb
index b1f69f043f2..1f0746d7726 100644
--- a/app/graphql/types/invitation_interface.rb
+++ b/app/graphql/types/invitation_interface.rb
@@ -4,7 +4,7 @@ module Types
module InvitationInterface
include BaseInterface
- field :email, GraphQL::STRING_TYPE, null: false,
+ field :email, GraphQL::Types::String, null: false,
description: 'Email of the member to invite.'
field :access_level, Types::AccessLevelType, null: true,
diff --git a/app/graphql/types/issue_status_counts_type.rb b/app/graphql/types/issue_status_counts_type.rb
index 77429f9ea12..e4a0782e3cf 100644
--- a/app/graphql/types/issue_status_counts_type.rb
+++ b/app/graphql/types/issue_status_counts_type.rb
@@ -15,7 +15,7 @@ module Types
next unless available_issue_states.include?(state.downcase)
field state,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: true,
description: "Number of issues with status #{state.upcase} for the project"
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 6ff38273c03..42feb8a8076 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -15,23 +15,23 @@ module Types
present_using IssuePresenter
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: "ID of the issue."
- field :iid, GraphQL::ID_TYPE, null: false,
+ field :iid, GraphQL::Types::ID, null: false,
description: "Internal ID of the issue."
- field :title, GraphQL::STRING_TYPE, null: false,
+ field :title, GraphQL::Types::String, null: false,
description: 'Title of the issue.'
markdown_field :title_html, null: true
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the issue.'
markdown_field :description_html, null: true
field :state, IssueStateEnum, null: false,
description: 'State of the issue.'
- field :reference, GraphQL::STRING_TYPE, null: false,
+ field :reference, GraphQL::Types::String, null: false,
description: 'Internal reference of the issue. Returned in shortened format by default.',
method: :to_reference do
- argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false,
+ argument :full, GraphQL::Types::Boolean, required: false, default_value: false,
description: 'Boolean option specifying whether the reference should be returned in full.'
end
@@ -51,42 +51,45 @@ module Types
field :due_date, Types::TimeType, null: true,
description: 'Due date of the issue.'
- field :confidential, GraphQL::BOOLEAN_TYPE, null: false,
+ field :confidential, GraphQL::Types::Boolean, null: false,
description: 'Indicates the issue is confidential.'
- field :discussion_locked, GraphQL::BOOLEAN_TYPE, null: false,
+ field :discussion_locked, GraphQL::Types::Boolean, null: false,
description: 'Indicates discussion is locked on the issue.'
- field :upvotes, GraphQL::INT_TYPE, null: false,
+ field :upvotes, GraphQL::Types::Int, null: false,
description: 'Number of upvotes the issue has received.'
- field :downvotes, GraphQL::INT_TYPE, null: false,
+ field :downvotes, GraphQL::Types::Int, null: false,
description: 'Number of downvotes the issue has received.'
- field :user_notes_count, GraphQL::INT_TYPE, null: false,
+ field :merge_requests_count, GraphQL::Types::Int, null: false,
+ description: 'Number of merge requests that close the issue on merge.',
+ resolver: Resolvers::MergeRequestsCountResolver
+ field :user_notes_count, GraphQL::Types::Int, null: false,
description: 'Number of user notes of the issue.',
resolver: Resolvers::UserNotesCountResolver
- field :user_discussions_count, GraphQL::INT_TYPE, null: false,
+ field :user_discussions_count, GraphQL::Types::Int, null: false,
description: 'Number of user discussions in the issue.',
resolver: Resolvers::UserDiscussionsCountResolver
- field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path,
+ field :web_path, GraphQL::Types::String, null: false, method: :issue_path,
description: 'Web path of the issue.'
- field :web_url, GraphQL::STRING_TYPE, null: false,
+ field :web_url, GraphQL::Types::String, null: false,
description: 'Web URL of the issue.'
- field :relative_position, GraphQL::INT_TYPE, null: true,
+ field :relative_position, GraphQL::Types::Int, null: true,
description: 'Relative position of the issue (used for positioning in epic tree and issue boards).'
field :participants, Types::UserType.connection_type, null: true, complexity: 5,
description: 'List of participants in the issue.'
- field :emails_disabled, GraphQL::BOOLEAN_TYPE, null: false,
+ field :emails_disabled, GraphQL::Types::Boolean, null: false,
method: :project_emails_disabled?,
description: 'Indicates if a project has email notifications disabled: `true` if email notifications are disabled.'
- field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
+ field :subscribed, GraphQL::Types::Boolean, method: :subscribed?, null: false, complexity: 5,
description: 'Indicates the currently logged in user is subscribed to the issue.'
- field :time_estimate, GraphQL::INT_TYPE, null: false,
+ field :time_estimate, GraphQL::Types::Int, null: false,
description: 'Time estimate of the issue.'
- field :total_time_spent, GraphQL::INT_TYPE, null: false,
+ field :total_time_spent, GraphQL::Types::Int, null: false,
description: 'Total time reported as spent on the issue.'
- field :human_time_estimate, GraphQL::STRING_TYPE, null: true,
+ field :human_time_estimate, GraphQL::Types::String, null: true,
description: 'Human-readable time estimate of the issue.'
- field :human_total_time_spent, GraphQL::STRING_TYPE, null: true,
+ field :human_total_time_spent, GraphQL::Types::String, null: true,
description: 'Human-readable total time reported as spent on the issue.'
field :closed_at, Types::TimeType, null: true,
@@ -115,19 +118,19 @@ module Types
field :severity, Types::IssuableSeverityEnum, null: true,
description: 'Severity level of the incident.'
- field :moved, GraphQL::BOOLEAN_TYPE, method: :moved?, null: true,
+ field :moved, GraphQL::Types::Boolean, method: :moved?, null: true,
description: 'Indicates if issue got moved from other project.'
field :moved_to, Types::IssueType, null: true,
description: 'Updated Issue after it got moved to another project.'
- field :create_note_email, GraphQL::STRING_TYPE, null: true,
+ field :create_note_email, GraphQL::Types::String, null: true,
description: 'User specific email address for the issue.'
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the issue.'
- field :project_id, GraphQL::INT_TYPE, null: false, method: :project_id,
+ field :project_id, GraphQL::Types::Int, null: false, method: :project_id,
description: 'ID of the issue project.'
def author
diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb
index 7dc45f78c99..6999ea270a2 100644
--- a/app/graphql/types/issue_type_enum.rb
+++ b/app/graphql/types/issue_type_enum.rb
@@ -5,7 +5,7 @@ module Types
graphql_name 'IssueType'
description 'Issue type'
- ::Issue.issue_types.keys.each do |issue_type|
+ ::WorkItem::Type.base_types.keys.each do |issue_type|
value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type"
end
end
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 88faf7e7074..e5125c554a4 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -5,21 +5,24 @@ module Types
class NegatedIssueFilterInputType < BaseInputObject
graphql_name 'NegatedIssueFilterInput'
- argument :iids, [GraphQL::STRING_TYPE],
+ argument :iids, [GraphQL::Types::String],
required: false,
description: 'List of IIDs of issues to exclude. For example, `[1, 2]`.'
- argument :label_name, [GraphQL::STRING_TYPE],
+ argument :label_name, [GraphQL::Types::String],
required: false,
description: 'Labels not applied to this issue.'
- argument :milestone_title, [GraphQL::STRING_TYPE],
+ argument :milestone_title, [GraphQL::Types::String],
required: false,
description: 'Milestone not applied to this issue.'
- argument :assignee_usernames, [GraphQL::STRING_TYPE],
+ argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users not assigned to the issue.'
- argument :assignee_id, GraphQL::STRING_TYPE,
+ argument :assignee_id, GraphQL::Types::String,
required: false,
description: 'ID of a user not assigned to the issues.'
+ argument :milestone_wildcard_id, ::Types::NegatedMilestoneWildcardIdEnum,
+ required: false,
+ description: 'Filter by negated milestone wildcard values.'
end
end
end
diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb
index 6fa115933ac..0cdfc178350 100644
--- a/app/graphql/types/jira_import_type.rb
+++ b/app/graphql/types/jira_import_type.rb
@@ -12,13 +12,13 @@ module Types
description: 'Timestamp of when the Jira import was scheduled.'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import.'
- field :jira_project_key, GraphQL::STRING_TYPE, null: false,
+ field :jira_project_key, GraphQL::Types::String, null: false,
description: 'Project key for the imported Jira project.'
- field :imported_issues_count, GraphQL::INT_TYPE, null: false,
+ field :imported_issues_count, GraphQL::Types::Int, null: false,
description: 'Count of issues that were successfully imported.'
- field :failed_to_import_count, GraphQL::INT_TYPE, null: false,
+ field :failed_to_import_count, GraphQL::Types::Int, null: false,
description: 'Count of issues that failed to import.'
- field :total_issue_count, GraphQL::INT_TYPE, null: false,
+ field :total_issue_count, GraphQL::Types::Int, null: false,
description: 'Total count of issues that were attempted to import.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/jira_user_type.rb b/app/graphql/types/jira_user_type.rb
index 7ccb7ad6791..6e1c349726c 100644
--- a/app/graphql/types/jira_user_type.rb
+++ b/app/graphql/types/jira_user_type.rb
@@ -6,17 +6,17 @@ module Types
class JiraUserType < BaseObject
graphql_name 'JiraUser'
- field :jira_account_id, GraphQL::STRING_TYPE, null: false,
+ field :jira_account_id, GraphQL::Types::String, null: false,
description: 'Account ID of the Jira user.'
- field :jira_display_name, GraphQL::STRING_TYPE, null: false,
+ field :jira_display_name, GraphQL::Types::String, null: false,
description: 'Display name of the Jira user.'
- field :jira_email, GraphQL::STRING_TYPE, null: true,
+ field :jira_email, GraphQL::Types::String, null: true,
description: 'Email of the Jira user, returned only for users with public emails.'
- field :gitlab_id, GraphQL::INT_TYPE, null: true,
+ field :gitlab_id, GraphQL::Types::Int, null: true,
description: 'ID of the matched GitLab user.'
- field :gitlab_username, GraphQL::STRING_TYPE, null: true,
+ field :gitlab_username, GraphQL::Types::String, null: true,
description: 'Username of the matched GitLab user.'
- field :gitlab_name, GraphQL::STRING_TYPE, null: true,
+ field :gitlab_name, GraphQL::Types::String, null: true,
description: 'Name of the matched GitLab user.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/jira_users_mapping_input_type.rb b/app/graphql/types/jira_users_mapping_input_type.rb
index 32640b9cb17..37fd05370c0 100644
--- a/app/graphql/types/jira_users_mapping_input_type.rb
+++ b/app/graphql/types/jira_users_mapping_input_type.rb
@@ -5,11 +5,11 @@ module Types
graphql_name 'JiraUsersMappingInputType'
argument :jira_account_id,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
required: true,
description: 'Jira account ID of the user.'
argument :gitlab_id,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
required: false,
description: 'ID of the GitLab user.'
end
diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb
index 4e8718a80da..bb2d561014e 100644
--- a/app/graphql/types/label_type.rb
+++ b/app/graphql/types/label_type.rb
@@ -8,16 +8,16 @@ module Types
authorize :read_label
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'Label ID.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the label (Markdown rendered as HTML for caching).'
markdown_field :description_html, null: true
- field :title, GraphQL::STRING_TYPE, null: false,
+ field :title, GraphQL::Types::String, null: false,
description: 'Content of the label.'
- field :color, GraphQL::STRING_TYPE, null: false,
+ field :color, GraphQL::Types::String, null: false,
description: 'Background color of the label.'
- field :text_color, GraphQL::STRING_TYPE, null: false,
+ field :text_color, GraphQL::Types::String, null: false,
description: 'Text color of the label.'
field :created_at, Types::TimeType, null: false,
description: 'When this label was created.'
diff --git a/app/graphql/types/member_interface.rb b/app/graphql/types/member_interface.rb
index 6a21e51fe28..c5623cd4710 100644
--- a/app/graphql/types/member_interface.rb
+++ b/app/graphql/types/member_interface.rb
@@ -4,7 +4,7 @@ module Types
module MemberInterface
include BaseInterface
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the member.'
field :access_level, Types::AccessLevelType, null: true,
diff --git a/app/graphql/types/merge_request_sort_enum.rb b/app/graphql/types/merge_request_sort_enum.rb
index 92a71998d91..d75eae6abc4 100644
--- a/app/graphql/types/merge_request_sort_enum.rb
+++ b/app/graphql/types/merge_request_sort_enum.rb
@@ -7,5 +7,7 @@ module Types
value 'MERGED_AT_ASC', 'Merge time by ascending order.', value: :merged_at_asc
value 'MERGED_AT_DESC', 'Merge time by descending order.', value: :merged_at_desc
+ value 'CLOSED_AT_ASC', 'Closed time by ascending order.', value: :closed_at_asc
+ value 'CLOSED_AT_DESC', 'Closed time by descending order.', value: :closed_at_desc
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 0e9df926cdd..8e6b5421ede 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -15,14 +15,14 @@ module Types
present_using MergeRequestPresenter
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the merge request.'
- field :iid, GraphQL::STRING_TYPE, null: false,
+ field :iid, GraphQL::Types::String, null: false,
description: 'Internal ID of the merge request.'
- field :title, GraphQL::STRING_TYPE, null: false,
+ field :title, GraphQL::Types::String, null: false,
description: 'Title of the merge request.'
markdown_field :title_html, null: true
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the merge request (Markdown rendered as HTML for caching).'
markdown_field :description_html, null: true
field :state, MergeRequestStateEnum, null: false,
@@ -41,91 +41,91 @@ module Types
description: 'References of the base SHA, the head SHA, and the start SHA for this merge request.'
field :project, Types::ProjectType, null: false,
description: 'Alias for target_project.'
- field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id,
+ field :project_id, GraphQL::Types::Int, null: false, method: :target_project_id,
description: 'ID of the merge request project.'
- field :source_project_id, GraphQL::INT_TYPE, null: true,
+ field :source_project_id, GraphQL::Types::Int, null: true,
description: 'ID of the merge request source project.'
- field :target_project_id, GraphQL::INT_TYPE, null: false,
+ field :target_project_id, GraphQL::Types::Int, null: false,
description: 'ID of the merge request target project.'
- field :source_branch, GraphQL::STRING_TYPE, null: false,
+ field :source_branch, GraphQL::Types::String, null: false,
description: 'Source branch of the merge request.'
- field :source_branch_protected, GraphQL::BOOLEAN_TYPE, null: false, calls_gitaly: true,
+ field :source_branch_protected, GraphQL::Types::Boolean, null: false, calls_gitaly: true,
description: 'Indicates if the source branch is protected.'
- field :target_branch, GraphQL::STRING_TYPE, null: false,
+ field :target_branch, GraphQL::Types::String, null: false,
description: 'Target branch of the merge request.'
- field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
+ field :work_in_progress, GraphQL::Types::Boolean, 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,
+ field :draft, GraphQL::Types::Boolean, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.'
- field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
+ field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS).'
- field :diff_head_sha, GraphQL::STRING_TYPE, null: true,
+ field :diff_head_sha, GraphQL::Types::String, null: true,
description: 'Diff head SHA of the merge request.'
field :diff_stats, [Types::DiffStatsType], null: true, calls_gitaly: true,
description: 'Details about which files were changed in this merge request.' do
- argument :path, GraphQL::STRING_TYPE, required: false, description: 'A specific file-path.'
+ argument :path, GraphQL::Types::String, required: false, description: 'A specific file-path.'
end
field :diff_stats_summary, Types::DiffStatsSummaryType, null: true, calls_gitaly: true,
description: 'Summary of which files were changed in this merge request.'
- field :merge_commit_sha, GraphQL::STRING_TYPE, null: true,
+ field :merge_commit_sha, GraphQL::Types::String, null: true,
description: 'SHA of the merge request commit (set once merged).'
- field :user_notes_count, GraphQL::INT_TYPE, null: true,
+ field :user_notes_count, GraphQL::Types::Int, null: true,
description: 'User notes count of the merge request.',
resolver: Resolvers::UserNotesCountResolver
- field :user_discussions_count, GraphQL::INT_TYPE, null: true,
+ field :user_discussions_count, GraphQL::Types::Int, null: true,
description: 'Number of user discussions in the merge request.',
resolver: Resolvers::UserDiscussionsCountResolver
- field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true,
+ field :should_remove_source_branch, GraphQL::Types::Boolean, method: :should_remove_source_branch?, null: true,
description: 'Indicates if the source branch of the merge request will be deleted after merge.'
- field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true,
+ field :force_remove_source_branch, GraphQL::Types::Boolean, method: :force_remove_source_branch?, null: true,
description: 'Indicates if the project settings will lead to source branch deletion after merge.'
- field :merge_status, GraphQL::STRING_TYPE, method: :public_merge_status, null: true,
+ field :merge_status, GraphQL::Types::String, method: :public_merge_status, null: true,
description: 'Status of the merge request.',
deprecated: { reason: :renamed, replacement: 'MergeRequest.mergeStatusEnum', milestone: '14.0' }
field :merge_status_enum, ::Types::MergeRequests::MergeStatusEnum,
method: :public_merge_status, null: true,
description: 'Merge status of the merge request.'
- field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true,
+ field :in_progress_merge_commit_sha, GraphQL::Types::String, null: true,
description: 'Commit SHA of the merge request if merge is in progress.'
- field :merge_error, GraphQL::STRING_TYPE, null: true,
+ field :merge_error, GraphQL::Types::String, null: true,
description: 'Error message due to a merge error.'
- field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true,
+ field :allow_collaboration, GraphQL::Types::Boolean, null: true,
description: 'Indicates if members of the target project can push to the fork.'
- field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false, calls_gitaly: true,
+ field :should_be_rebased, GraphQL::Types::Boolean, method: :should_be_rebased?, null: false, calls_gitaly: true,
description: 'Indicates if the merge request will be rebased.'
- field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true,
+ field :rebase_commit_sha, GraphQL::Types::String, null: true,
description: 'Rebase commit SHA of the merge request.'
- field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true,
+ field :rebase_in_progress, GraphQL::Types::Boolean, method: :rebase_in_progress?, null: false, calls_gitaly: true,
description: 'Indicates if there is a rebase currently in progress for the merge request.'
- field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true,
+ field :default_merge_commit_message, GraphQL::Types::String, null: true,
description: 'Default merge commit message of the merge request.'
- field :default_merge_commit_message_with_description, GraphQL::STRING_TYPE, null: true,
+ field :default_merge_commit_message_with_description, GraphQL::Types::String, null: true,
description: 'Default merge commit message of the merge request with description.'
- field :default_squash_commit_message, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ field :default_squash_commit_message, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default squash commit message of the merge request.'
- field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false,
+ field :merge_ongoing, GraphQL::Types::Boolean, method: :merge_ongoing?, null: false,
description: 'Indicates if a merge is currently occurring.'
- field :source_branch_exists, GraphQL::BOOLEAN_TYPE,
+ field :source_branch_exists, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :source_branch_exists?,
description: 'Indicates if the source branch of the merge request exists.'
- field :target_branch_exists, GraphQL::BOOLEAN_TYPE,
+ field :target_branch_exists, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :target_branch_exists?,
description: 'Indicates if the target branch of the merge request exists.'
- field :diverged_from_target_branch, GraphQL::BOOLEAN_TYPE,
+ field :diverged_from_target_branch, GraphQL::Types::Boolean,
null: false, calls_gitaly: true,
method: :diverged_from_target_branch?,
description: 'Indicates if the source branch is behind the target branch.'
- field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true,
+ field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true,
description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged.'
- field :web_url, GraphQL::STRING_TYPE, null: true,
+ field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the merge request.'
- field :upvotes, GraphQL::INT_TYPE, null: false,
+ field :upvotes, GraphQL::Types::Int, null: false,
description: 'Number of upvotes for the merge request.'
- field :downvotes, GraphQL::INT_TYPE, null: false,
+ field :downvotes, GraphQL::Types::Int, null: false,
description: 'Number of downvotes for the merge request.'
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline,
@@ -151,52 +151,52 @@ module Types
description: 'User who created this merge request.'
field :participants, Types::UserType.connection_type, null: true, complexity: 15,
description: 'Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes.'
- field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
+ field :subscribed, GraphQL::Types::Boolean, method: :subscribed?, null: false, complexity: 5,
description: 'Indicates if the currently logged in user is subscribed to this merge request.'
field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
description: 'Labels of the merge request.'
- field :discussion_locked, GraphQL::BOOLEAN_TYPE,
+ field :discussion_locked, GraphQL::Types::Boolean,
description: 'Indicates if comments on the merge request are locked to members only.',
null: false
- field :time_estimate, GraphQL::INT_TYPE, null: false,
+ field :time_estimate, GraphQL::Types::Int, null: false,
description: 'Time estimate of the merge request.'
- field :total_time_spent, GraphQL::INT_TYPE, null: false,
+ field :total_time_spent, GraphQL::Types::Int, null: false,
description: 'Total time reported as spent on the merge request.'
- field :human_time_estimate, GraphQL::STRING_TYPE, null: true,
+ field :human_time_estimate, GraphQL::Types::String, null: true,
description: 'Human-readable time estimate of the merge request.'
- field :human_total_time_spent, GraphQL::STRING_TYPE, null: true,
+ field :human_total_time_spent, GraphQL::Types::String, null: true,
description: 'Human-readable total time reported as spent on the merge request.'
- field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference,
+ field :reference, GraphQL::Types::String, null: false, method: :to_reference,
description: 'Internal reference of the merge request. Returned in shortened format by default.' do
- argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false,
+ argument :full, GraphQL::Types::Boolean, required: false, default_value: false,
description: 'Boolean option specifying whether the reference should be returned in full.'
end
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
- field :commit_count, GraphQL::INT_TYPE, null: true, method: :commits_count,
+ field :commit_count, GraphQL::Types::Int, null: true, method: :commits_count,
description: 'Number of commits in the merge request.'
- field :conflicts, GraphQL::BOOLEAN_TYPE, null: false, method: :cannot_be_merged?,
+ field :conflicts, GraphQL::Types::Boolean, null: false, method: :cannot_be_merged?,
description: 'Indicates if the merge request has conflicts.'
- field :auto_merge_enabled, GraphQL::BOOLEAN_TYPE, null: false,
+ field :auto_merge_enabled, GraphQL::Types::Boolean, null: false,
description: 'Indicates if auto merge is enabled for the merge request.'
field :approved_by, Types::UserType.connection_type, null: true,
description: 'Users who approved the merge request.'
- field :squash_on_merge, GraphQL::BOOLEAN_TYPE, null: false, method: :squash_on_merge?,
+ field :squash_on_merge, GraphQL::Types::Boolean, null: false, method: :squash_on_merge?,
description: 'Indicates if squash on merge is enabled.'
- field :squash, GraphQL::BOOLEAN_TYPE, null: false,
+ field :squash, GraphQL::Types::Boolean, null: false,
description: 'Indicates if squash on merge is enabled.'
- field :available_auto_merge_strategies, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
+ field :available_auto_merge_strategies, [GraphQL::Types::String], null: true, calls_gitaly: true,
description: 'Array of available auto merge strategies.'
- field :has_ci, GraphQL::BOOLEAN_TYPE, null: false, method: :has_ci?,
+ field :has_ci, GraphQL::Types::Boolean, null: false, method: :has_ci?,
description: 'Indicates if the merge request has CI.'
- field :mergeable, GraphQL::BOOLEAN_TYPE, null: false, method: :mergeable?, calls_gitaly: true,
+ field :mergeable, GraphQL::Types::Boolean, null: false, method: :mergeable?, calls_gitaly: true,
description: 'Indicates if the merge request is mergeable.'
field :commits_without_merge_commits, Types::CommitType.connection_type, null: true,
calls_gitaly: true, description: 'Merge request commits excluding merge commits.'
- field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true,
+ field :security_auto_fix, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.'
- field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true,
+ field :auto_merge_strategy, GraphQL::Types::String, null: true,
description: 'Selected auto merge strategy.'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request.'
diff --git a/app/graphql/types/metadata/kas_type.rb b/app/graphql/types/metadata/kas_type.rb
index 8af4c23270b..a947986fa60 100644
--- a/app/graphql/types/metadata/kas_type.rb
+++ b/app/graphql/types/metadata/kas_type.rb
@@ -7,11 +7,11 @@ module Types
authorize :read_instance_metadata
- field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
+ field :enabled, GraphQL::Types::Boolean, null: false,
description: 'Indicates whether the Kubernetes Agent Server is enabled.'
- field :version, GraphQL::STRING_TYPE, null: true,
+ field :version, GraphQL::Types::String, null: true,
description: 'KAS version.'
- field :external_url, GraphQL::STRING_TYPE, null: true,
+ field :external_url, GraphQL::Types::String, null: true,
description: 'The URL used by the Agents to communicate with KAS.'
end
end
diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb
index 851c2a3f1e3..ed1e697711d 100644
--- a/app/graphql/types/metadata_type.rb
+++ b/app/graphql/types/metadata_type.rb
@@ -6,9 +6,9 @@ module Types
authorize :read_instance_metadata
- field :version, GraphQL::STRING_TYPE, null: false,
+ field :version, GraphQL::Types::String, null: false,
description: 'Version.'
- field :revision, GraphQL::STRING_TYPE, null: false,
+ field :revision, GraphQL::Types::String, null: false,
description: 'Revision.'
field :kas, ::Types::Metadata::KasType, null: false,
description: 'Metadata about KAS.'
diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb
index 40d2c2f195c..04cac55894e 100644
--- a/app/graphql/types/metrics/dashboard_type.rb
+++ b/app/graphql/types/metrics/dashboard_type.rb
@@ -7,10 +7,10 @@ module Types
class DashboardType < ::Types::BaseObject
graphql_name 'MetricsDashboard'
- field :path, GraphQL::STRING_TYPE, null: true,
+ field :path, GraphQL::Types::String, null: true,
description: 'Path to a file with the dashboard definition.'
- field :schema_validation_warnings, [GraphQL::STRING_TYPE], null: true,
+ field :schema_validation_warnings, [GraphQL::Types::String], null: true,
description: 'Dashboard schema validation warnings.'
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
diff --git a/app/graphql/types/metrics/dashboards/annotation_type.rb b/app/graphql/types/metrics/dashboards/annotation_type.rb
index b9e040dd063..fb35f2bd9a1 100644
--- a/app/graphql/types/metrics/dashboards/annotation_type.rb
+++ b/app/graphql/types/metrics/dashboards/annotation_type.rb
@@ -7,13 +7,13 @@ module Types
authorize :read_metrics_dashboard_annotation
graphql_name 'MetricsDashboardAnnotation'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the annotation.'
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the annotation.'
- field :panel_id, GraphQL::STRING_TYPE, null: true,
+ field :panel_id, GraphQL::Types::String, null: true,
description: 'ID of a dashboard panel to which the annotation should be scoped.'
field :starting_at, Types::TimeType, null: true,
diff --git a/app/graphql/types/milestone_stats_type.rb b/app/graphql/types/milestone_stats_type.rb
index e313b880e0d..6d8b7deb8e7 100644
--- a/app/graphql/types/milestone_stats_type.rb
+++ b/app/graphql/types/milestone_stats_type.rb
@@ -7,10 +7,10 @@ module Types
authorize :read_milestone
- field :total_issues_count, GraphQL::INT_TYPE, null: true,
+ field :total_issues_count, GraphQL::Types::Int, null: true,
description: 'Total number of issues associated with the milestone.'
- field :closed_issues_count, GraphQL::INT_TYPE, null: true,
+ field :closed_issues_count, GraphQL::Types::Int, null: true,
description: 'Number of closed issues associated with the milestone.'
end
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index 27bc77b4da1..18e4a5d33e3 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -11,25 +11,25 @@ module Types
alias_method :milestone, :object
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the milestone.'
- field :iid, GraphQL::ID_TYPE, null: false,
+ field :iid, GraphQL::Types::ID, null: false,
description: "Internal ID of the milestone."
- field :title, GraphQL::STRING_TYPE, null: false,
+ field :title, GraphQL::Types::String, null: false,
description: 'Title of the milestone.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the milestone.'
field :state, Types::MilestoneStateEnum, null: false,
description: 'State of the milestone.'
- field :expired, GraphQL::BOOLEAN_TYPE, null: false,
+ field :expired, GraphQL::Types::Boolean, null: false,
description: 'Expired state of the milestone (a milestone is expired when the due date is past the current date). Defaults to `false` when due date has not been set.'
- field :web_path, GraphQL::STRING_TYPE, null: false, method: :milestone_path,
+ field :web_path, GraphQL::Types::String, null: false, method: :milestone_path,
description: 'Web path of the milestone.'
field :due_date, Types::TimeType, null: true,
@@ -44,15 +44,15 @@ module Types
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of last milestone update.'
- field :project_milestone, GraphQL::BOOLEAN_TYPE, null: false,
+ field :project_milestone, GraphQL::Types::Boolean, null: false,
description: 'Indicates if milestone is at project level.',
method: :project_milestone?
- field :group_milestone, GraphQL::BOOLEAN_TYPE, null: false,
+ field :group_milestone, GraphQL::Types::Boolean, null: false,
description: 'Indicates if milestone is at group level.',
method: :group_milestone?
- field :subgroup_milestone, GraphQL::BOOLEAN_TYPE, null: false,
+ field :subgroup_milestone, GraphQL::Types::Boolean, null: false,
description: 'Indicates if milestone is at subgroup level.',
method: :subgroup_milestone?
diff --git a/app/graphql/types/milestone_wildcard_id_enum.rb b/app/graphql/types/milestone_wildcard_id_enum.rb
new file mode 100644
index 00000000000..b5b339b1e5b
--- /dev/null
+++ b/app/graphql/types/milestone_wildcard_id_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ class MilestoneWildcardIdEnum < BaseEnum
+ graphql_name 'MilestoneWildcardId'
+ description 'Milestone ID wildcard values'
+
+ value 'NONE', 'No milestone is assigned.'
+ value 'ANY', 'A milestone is assigned.'
+ value 'STARTED', 'An open, started milestone (start date <= today).'
+ value 'UPCOMING', 'An open milestone due in the future (due date >= today).'
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index df693fafbb9..293d19d068a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -99,14 +99,18 @@ module Types
mount_mutation Mutations::Ci::CiCdSettingsUpdate
mount_mutation Mutations::Ci::Job::Play
mount_mutation Mutations::Ci::Job::Retry
+ mount_mutation Mutations::Ci::Job::Cancel
+ mount_mutation Mutations::Ci::Job::Unschedule
mount_mutation Mutations::Ci::JobTokenScope::AddProject
mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query
mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query
mount_mutation Mutations::Namespace::PackageSettings::Update
+ mount_mutation Mutations::Groups::Update
mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::Packages::Destroy
+ mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Echo
end
end
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index af091515979..d573cc9ded5 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -8,9 +8,9 @@ module Types
authorize :read_package_settings
- field :maven_duplicates_allowed, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
+ field :maven_duplicates_allowed, GraphQL::Types::Boolean, 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_duplicates_allowed, GraphQL::Types::Boolean, 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/shared_runners_setting_enum.rb b/app/graphql/types/namespace/shared_runners_setting_enum.rb
new file mode 100644
index 00000000000..4773e414aeb
--- /dev/null
+++ b/app/graphql/types/namespace/shared_runners_setting_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ class Namespace::SharedRunnersSettingEnum < BaseEnum
+ graphql_name 'SharedRunnersSetting'
+
+ ::Namespace::SHARED_RUNNERS_SETTINGS.each do |type|
+ value type.upcase,
+ description: "Sharing of runners is #{type.tr('_', ' ')}.",
+ value: type
+ end
+ end
+end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index 96eff8a46b0..4cc543f477a 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -6,27 +6,27 @@ module Types
authorize :read_namespace
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the namespace.'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name of the namespace.'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'Path of the namespace.'
- field :full_name, GraphQL::STRING_TYPE, null: false,
+ field :full_name, GraphQL::Types::String, null: false,
description: 'Full name of the namespace.'
- field :full_path, GraphQL::ID_TYPE, null: false,
+ field :full_path, GraphQL::Types::ID, null: false,
description: 'Full path of the namespace.'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description of the namespace.'
markdown_field :description_html, null: true
- field :visibility, GraphQL::STRING_TYPE, null: true,
+ field :visibility, GraphQL::Types::String, null: true,
description: 'Visibility of the namespace.'
- field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?,
+ field :lfs_enabled, GraphQL::Types::Boolean, null: true, method: :lfs_enabled?,
description: 'Indicates if Large File Storage (LFS) is enabled for namespace.'
- field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :request_access_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if users can request access to namespace.'
field :root_storage_statistics, Types::RootStorageStatisticsType,
@@ -42,6 +42,11 @@ module Types
null: true,
description: 'The package settings for the namespace.'
+ field :shared_runners_setting,
+ Types::Namespace::SharedRunnersSettingEnum,
+ null: true,
+ description: "Shared runners availability for the namespace and its descendants."
+
def root_storage_statistics
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
end
diff --git a/app/graphql/types/negated_milestone_wildcard_id_enum.rb b/app/graphql/types/negated_milestone_wildcard_id_enum.rb
new file mode 100644
index 00000000000..ca27a6c7b6e
--- /dev/null
+++ b/app/graphql/types/negated_milestone_wildcard_id_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class NegatedMilestoneWildcardIdEnum < BaseEnum
+ graphql_name 'NegatedMilestoneWildcardId'
+ description 'Negated Milestone ID wildcard values'
+
+ value 'STARTED', 'An open, started milestone (start date <= today).'
+ value 'UPCOMING', 'An open milestone due in the future (due date >= today).'
+ end
+end
diff --git a/app/graphql/types/notes/diff_image_position_input_type.rb b/app/graphql/types/notes/diff_image_position_input_type.rb
index dd5c8f20cc3..d56c67bbec8 100644
--- a/app/graphql/types/notes/diff_image_position_input_type.rb
+++ b/app/graphql/types/notes/diff_image_position_input_type.rb
@@ -5,13 +5,13 @@ module Types
class DiffImagePositionInputType < DiffPositionBaseInputType
graphql_name 'DiffImagePositionInput'
- argument :x, GraphQL::INT_TYPE, required: true,
+ argument :x, GraphQL::Types::Int, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :x)
- argument :y, GraphQL::INT_TYPE, required: true,
+ argument :y, GraphQL::Types::Int, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :y)
- argument :width, GraphQL::INT_TYPE, required: true,
+ argument :width, GraphQL::Types::Int, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :width)
- argument :height, GraphQL::INT_TYPE, required: true,
+ argument :height, GraphQL::Types::Int, required: true,
description: copy_field_description(Types::Notes::DiffPositionType, :height)
end
end
diff --git a/app/graphql/types/notes/diff_position_base_input_type.rb b/app/graphql/types/notes/diff_position_base_input_type.rb
index c8f9f9028cc..e773fbbc8a1 100644
--- a/app/graphql/types/notes/diff_position_base_input_type.rb
+++ b/app/graphql/types/notes/diff_position_base_input_type.rb
@@ -3,11 +3,11 @@
module Types
module Notes
class DiffPositionBaseInputType < BaseInputObject
- argument :head_sha, GraphQL::STRING_TYPE, required: true,
+ argument :head_sha, GraphQL::Types::String, required: true,
description: copy_field_description(Types::DiffRefsType, :head_sha)
- argument :base_sha, GraphQL::STRING_TYPE, required: false,
+ argument :base_sha, GraphQL::Types::String, required: false,
description: copy_field_description(Types::DiffRefsType, :base_sha)
- argument :start_sha, GraphQL::STRING_TYPE, required: true,
+ argument :start_sha, GraphQL::Types::String, required: true,
description: copy_field_description(Types::DiffRefsType, :start_sha)
argument :paths,
diff --git a/app/graphql/types/notes/diff_position_input_type.rb b/app/graphql/types/notes/diff_position_input_type.rb
index 7ec5fd9e086..18ce6672d14 100644
--- a/app/graphql/types/notes/diff_position_input_type.rb
+++ b/app/graphql/types/notes/diff_position_input_type.rb
@@ -5,9 +5,9 @@ module Types
class DiffPositionInputType < DiffPositionBaseInputType
graphql_name 'DiffPositionInput'
- argument :old_line, GraphQL::INT_TYPE, required: false,
+ argument :old_line, GraphQL::Types::Int, required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :old_line)
- argument :new_line, GraphQL::INT_TYPE, required: false,
+ argument :new_line, GraphQL::Types::Int, required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :new_line)
end
end
diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb
index 67747a13dcf..9c756d56b97 100644
--- a/app/graphql/types/notes/diff_position_type.rb
+++ b/app/graphql/types/notes/diff_position_type.rb
@@ -10,29 +10,29 @@ module Types
field :diff_refs, Types::DiffRefsType, null: false,
description: 'Information about the branch, HEAD, and base at the time of commenting.'
- field :file_path, GraphQL::STRING_TYPE, null: false,
+ field :file_path, GraphQL::Types::String, null: false,
description: 'Path of the file that was changed.'
- field :old_path, GraphQL::STRING_TYPE, null: true,
+ field :old_path, GraphQL::Types::String, null: true,
description: 'Path of the file on the start SHA.'
- field :new_path, GraphQL::STRING_TYPE, null: true,
+ field :new_path, GraphQL::Types::String, null: true,
description: 'Path of the file on the HEAD SHA.'
field :position_type, Types::Notes::PositionTypeEnum, null: false,
description: 'Type of file the position refers to.'
# Fields for text positions
- field :old_line, GraphQL::INT_TYPE, null: true,
+ field :old_line, GraphQL::Types::Int, null: true,
description: 'Line on start SHA that was changed.'
- field :new_line, GraphQL::INT_TYPE, null: true,
+ field :new_line, GraphQL::Types::Int, null: true,
description: 'Line on HEAD SHA that was changed.'
# Fields for image positions
- field :x, GraphQL::INT_TYPE, null: true,
+ field :x, GraphQL::Types::Int, null: true,
description: 'X position of the note.'
- field :y, GraphQL::INT_TYPE, null: true,
+ field :y, GraphQL::Types::Int, null: true,
description: 'Y position of the note.'
- field :width, GraphQL::INT_TYPE, null: true,
+ field :width, GraphQL::Types::Int, null: true,
description: 'Total width of the image.'
- field :height, GraphQL::INT_TYPE, null: true,
+ field :height, GraphQL::Types::Int, null: true,
description: 'Total height of the image.'
def old_line
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
index 751cf7c10f1..fa33428114c 100644
--- a/app/graphql/types/notes/note_type.rb
+++ b/app/graphql/types/notes/note_type.rb
@@ -22,13 +22,13 @@ module Types
null: false,
description: 'User who wrote this note.'
- field :system, GraphQL::BOOLEAN_TYPE,
+ field :system, GraphQL::Types::Boolean,
null: false,
description: 'Indicates whether this note was created by the system or by a user.'
- field :system_note_icon_name, GraphQL::STRING_TYPE, null: true,
+ field :system_note_icon_name, GraphQL::Types::String, null: true,
description: 'Name of the icon corresponding to a system note.'
- field :body, GraphQL::STRING_TYPE,
+ field :body, GraphQL::Types::String,
null: false,
method: :note,
description: 'Content of the note.'
@@ -43,10 +43,10 @@ module Types
description: 'The discussion this note is a part of.'
field :position, Types::Notes::DiffPositionType, null: true,
description: 'The position of this note on a diff.'
- field :confidential, GraphQL::BOOLEAN_TYPE, null: true,
+ field :confidential, GraphQL::Types::Boolean, null: true,
description: 'Indicates if this note is confidential.',
method: :confidential?
- field :url, GraphQL::STRING_TYPE,
+ field :url, GraphQL::Types::String,
null: true,
description: 'URL to view this Note in the Web UI.'
diff --git a/app/graphql/types/notes/update_diff_image_position_input_type.rb b/app/graphql/types/notes/update_diff_image_position_input_type.rb
index ab27f6b9ad3..0c6e4a16434 100644
--- a/app/graphql/types/notes/update_diff_image_position_input_type.rb
+++ b/app/graphql/types/notes/update_diff_image_position_input_type.rb
@@ -6,19 +6,19 @@ module Types
class UpdateDiffImagePositionInputType < BaseInputObject
graphql_name 'UpdateDiffImagePositionInput'
- argument :x, GraphQL::INT_TYPE,
+ argument :x, GraphQL::Types::Int,
required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :x)
- argument :y, GraphQL::INT_TYPE,
+ argument :y, GraphQL::Types::Int,
required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :y)
- argument :width, GraphQL::INT_TYPE,
+ argument :width, GraphQL::Types::Int,
required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :width)
- argument :height, GraphQL::INT_TYPE,
+ argument :height, GraphQL::Types::Int,
required: false,
description: copy_field_description(Types::Notes::DiffPositionType, :height)
diff --git a/app/graphql/types/packages/composer/json_type.rb b/app/graphql/types/packages/composer/json_type.rb
index b7aa32f0170..d2bd62ca74d 100644
--- a/app/graphql/types/packages/composer/json_type.rb
+++ b/app/graphql/types/packages/composer/json_type.rb
@@ -8,10 +8,10 @@ module Types
graphql_name 'PackageComposerJsonType'
description 'Represents a composer JSON file'
- field :name, GraphQL::STRING_TYPE, null: true, description: 'The name set in the Composer JSON file.'
- field :type, GraphQL::STRING_TYPE, null: true, description: 'The type set in the Composer JSON file.'
- field :license, GraphQL::STRING_TYPE, null: true, description: 'The license set in the Composer JSON file.'
- field :version, GraphQL::STRING_TYPE, null: true, description: 'The version set in the Composer JSON file.'
+ field :name, GraphQL::Types::String, null: true, description: 'The name set in the Composer JSON file.'
+ field :type, GraphQL::Types::String, null: true, description: 'The type set in the Composer JSON file.'
+ field :license, GraphQL::Types::String, null: true, description: 'The license set in the Composer JSON file.'
+ field :version, GraphQL::Types::String, null: true, description: 'The version set in the Composer JSON file.'
end
end
end
diff --git a/app/graphql/types/packages/composer/metadatum_type.rb b/app/graphql/types/packages/composer/metadatum_type.rb
index 9d4ce3cebd4..092e729ec56 100644
--- a/app/graphql/types/packages/composer/metadatum_type.rb
+++ b/app/graphql/types/packages/composer/metadatum_type.rb
@@ -9,7 +9,7 @@ module Types
authorize :read_package
- field :target_sha, GraphQL::STRING_TYPE, null: false, description: 'Target SHA of the package.'
+ field :target_sha, GraphQL::Types::String, null: false, description: 'Target SHA of the package.'
field :composer_json, Types::Packages::Composer::JsonType, null: false, description: 'Data of the Composer JSON file.'
end
end
diff --git a/app/graphql/types/packages/conan/file_metadatum_type.rb b/app/graphql/types/packages/conan/file_metadatum_type.rb
index 97d5abe6ba4..9a26fd5de51 100644
--- a/app/graphql/types/packages/conan/file_metadatum_type.rb
+++ b/app/graphql/types/packages/conan/file_metadatum_type.rb
@@ -12,9 +12,9 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Conan::FileMetadatum], null: false, description: 'ID of the metadatum.'
- field :recipe_revision, GraphQL::STRING_TYPE, null: false, description: 'Revision of the Conan recipe.'
- field :package_revision, GraphQL::STRING_TYPE, null: true, description: 'Revision of the package.'
- field :conan_package_reference, GraphQL::STRING_TYPE, null: true, description: 'Reference of the Conan package.'
+ field :recipe_revision, GraphQL::Types::String, null: false, description: 'Revision of the Conan recipe.'
+ field :package_revision, GraphQL::Types::String, null: true, description: 'Revision of the package.'
+ field :conan_package_reference, GraphQL::Types::String, null: true, description: 'Reference of the Conan package.'
field :conan_file_type, ::Types::Packages::Conan::MetadatumFileTypeEnum, null: false, description: 'Type of the Conan file.'
end
end
diff --git a/app/graphql/types/packages/conan/metadatum_type.rb b/app/graphql/types/packages/conan/metadatum_type.rb
index 00b84235d27..cdfd0aa4483 100644
--- a/app/graphql/types/packages/conan/metadatum_type.rb
+++ b/app/graphql/types/packages/conan/metadatum_type.rb
@@ -12,10 +12,10 @@ module Types
field :id, ::Types::GlobalIDType[::Packages::Conan::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 :package_username, GraphQL::STRING_TYPE, null: false, description: 'Username of the Conan package.'
- field :package_channel, GraphQL::STRING_TYPE, null: false, description: 'Channel of the Conan package.'
- field :recipe, GraphQL::STRING_TYPE, null: false, description: 'Recipe of the Conan package.'
- field :recipe_path, GraphQL::STRING_TYPE, null: false, description: 'Recipe path of the Conan package.'
+ field :package_username, GraphQL::Types::String, null: false, description: 'Username of the Conan package.'
+ field :package_channel, GraphQL::Types::String, null: false, description: 'Channel of the Conan package.'
+ field :recipe, GraphQL::Types::String, null: false, description: 'Recipe of the Conan package.'
+ field :recipe_path, GraphQL::Types::String, null: false, description: 'Recipe path of the Conan package.'
end
end
end
diff --git a/app/graphql/types/packages/dependency_link_metadata_type.rb b/app/graphql/types/packages/dependency_link_metadata_type.rb
new file mode 100644
index 00000000000..c13069c7889
--- /dev/null
+++ b/app/graphql/types/packages/dependency_link_metadata_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class DependencyLinkMetadataType < BaseUnion
+ graphql_name 'DependencyLinkMetadata'
+ description 'Represents metadata associated with a dependency link'
+
+ possible_types ::Types::Packages::Nuget::DependencyLinkMetadatumType
+
+ def self.resolve_type(object, context)
+ case object
+ when ::Packages::Nuget::DependencyLinkMetadatum
+ ::Types::Packages::Nuget::DependencyLinkMetadatumType
+ else
+ # NOTE: This method must be kept in sync with `PackageDependencyLinkType#metadata`,
+ # which must never produce data that this discriminator cannot handle.
+ raise 'Unsupported metadata type'
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/maven/metadatum_type.rb b/app/graphql/types/packages/maven/metadatum_type.rb
index bdb250ef96b..eb3829648d1 100644
--- a/app/graphql/types/packages/maven/metadatum_type.rb
+++ b/app/graphql/types/packages/maven/metadatum_type.rb
@@ -12,10 +12,10 @@ module Types
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.'
+ field :path, GraphQL::Types::String, null: false, description: 'Path of the Maven package.'
+ field :app_group, GraphQL::Types::String, null: false, description: 'App group of the Maven package.'
+ field :app_version, GraphQL::Types::String, null: true, description: 'App version of the Maven package.'
+ field :app_name, GraphQL::Types::String, null: false, description: 'App name of the Maven package.'
end
end
end
diff --git a/app/graphql/types/packages/nuget/dependency_link_metadatum_type.rb b/app/graphql/types/packages/nuget/dependency_link_metadatum_type.rb
new file mode 100644
index 00000000000..f410e62b56a
--- /dev/null
+++ b/app/graphql/types/packages/nuget/dependency_link_metadatum_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Nuget
+ class DependencyLinkMetadatumType < BaseObject
+ graphql_name 'NugetDependencyLinkMetadata'
+ description 'Nuget dependency link metadata'
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Nuget::DependencyLinkMetadatum], null: false, description: 'ID of the metadatum.'
+ field :target_framework, GraphQL::Types::String, null: false, description: 'Target framework of the dependency link package.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb
index 63fae2fb197..ed9d97724af 100644
--- a/app/graphql/types/packages/nuget/metadatum_type.rb
+++ b/app/graphql/types/packages/nuget/metadatum_type.rb
@@ -10,9 +10,9 @@ module Types
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.'
+ field :license_url, GraphQL::Types::String, null: false, description: 'License URL of the Nuget package.'
+ field :project_url, GraphQL::Types::String, null: false, description: 'Project URL of the Nuget package.'
+ field :icon_url, GraphQL::Types::String, null: false, description: 'Icon URL of the Nuget package.'
end
end
end
diff --git a/app/graphql/types/packages/package_dependency_link_type.rb b/app/graphql/types/packages/package_dependency_link_type.rb
new file mode 100644
index 00000000000..eceb8319748
--- /dev/null
+++ b/app/graphql/types/packages/package_dependency_link_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageDependencyLinkType < BaseObject
+ graphql_name 'PackageDependencyLink'
+ description 'Represents a package dependency link'
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::DependencyLink], null: false, description: 'ID of the dependency link.'
+ field :dependency_type, Types::Packages::PackageDependencyTypeEnum, null: false, description: 'Dependency type.'
+ field :dependency, Types::Packages::PackageDependencyType, null: true, description: 'Dependency.'
+ field :metadata, Types::Packages::DependencyLinkMetadataType, null: true, description: 'Dependency link metadata.'
+
+ # NOTE: This method must be kept in sync with the union
+ # type: `Types::Packages::DependencyLinkMetadata`.
+ #
+ # `Types::Packages::DependencyLinkMetadata.resolve_type(metadata, ctx)` must never raise.
+ def metadata
+ model_class = case object.package.package_type
+ when 'nuget'
+ ::Packages::Nuget::DependencyLinkMetadatum
+ end
+
+ return unless model_class
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ BatchLoader::GraphQL.for(object.id).batch do |ids, loader|
+ results = model_class.where(dependency_link_id: ids)
+ results.each { |record| loader.call(record.dependency_link_id, record) }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ def dependency
+ ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Packages::Dependency, object.dependency_id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_dependency_type.rb b/app/graphql/types/packages/package_dependency_type.rb
new file mode 100644
index 00000000000..dcbc9fa3784
--- /dev/null
+++ b/app/graphql/types/packages/package_dependency_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+# this model does not have any kind of authorization so we disable it
+# rubocop:disable Graphql/AuthorizeTypes
+
+module Types
+ module Packages
+ class PackageDependencyType < BaseObject
+ graphql_name 'PackageDependency'
+ description 'Represents a package dependency.'
+
+ field :id, ::Types::GlobalIDType[::Packages::Dependency], null: false, description: 'ID of the dependency.'
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the dependency.'
+ field :version_pattern, GraphQL::Types::String, null: false, description: 'Version pattern of the dependency.'
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_dependency_type_enum.rb b/app/graphql/types/packages/package_dependency_type_enum.rb
new file mode 100644
index 00000000000..0e98d149b9a
--- /dev/null
+++ b/app/graphql/types/packages/package_dependency_type_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageDependencyTypeEnum < BaseEnum
+ graphql_name 'PackageDependencyType'
+
+ ::Packages::DependencyLink.dependency_types.keys.each do |type|
+ value type.to_s.underscore.upcase, description: "#{type} dependency type", value: type.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb
index 510b7e2ba41..f52b1f02519 100644
--- a/app/graphql/types/packages/package_details_type.rb
+++ b/app/graphql/types/packages/package_details_type.rb
@@ -12,6 +12,8 @@ module Types
field :package_files, Types::Packages::PackageFileType.connection_type, null: true, description: 'Package files.'
+ field :dependency_links, Types::Packages::PackageDependencyLinkType.connection_type, null: true, description: 'Dependency link.'
+
def versions
object.versions
end
diff --git a/app/graphql/types/packages/package_file_type.rb b/app/graphql/types/packages/package_file_type.rb
index e9e38559626..f77c40de8d8 100644
--- a/app/graphql/types/packages/package_file_type.rb
+++ b/app/graphql/types/packages/package_file_type.rb
@@ -10,12 +10,12 @@ module Types
field :id, ::Types::GlobalIDType[::Packages::PackageFile], null: false, description: 'ID of the file.'
field :created_at, Types::TimeType, null: false, description: 'The created date.'
field :updated_at, Types::TimeType, null: false, description: 'The updated date.'
- field :size, GraphQL::STRING_TYPE, null: false, description: 'Size of the package file.'
- field :file_name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package file.'
- field :download_path, GraphQL::STRING_TYPE, null: false, description: 'Download path of the package file.'
- field :file_md5, GraphQL::STRING_TYPE, null: true, description: 'Md5 of the package file.'
- field :file_sha1, GraphQL::STRING_TYPE, null: true, description: 'Sha1 of the package file.'
- field :file_sha256, GraphQL::STRING_TYPE, null: true, description: 'Sha256 of the package file.'
+ field :size, GraphQL::Types::String, null: false, description: 'Size of the package file.'
+ field :file_name, GraphQL::Types::String, null: false, description: 'Name of the package file.'
+ field :download_path, GraphQL::Types::String, null: false, description: 'Download path of the package file.'
+ field :file_md5, GraphQL::Types::String, null: true, description: 'Md5 of the package file.'
+ field :file_sha1, GraphQL::Types::String, null: true, description: 'Sha1 of the package file.'
+ field :file_sha256, GraphQL::Types::String, null: true, description: 'Sha256 of the package file.'
field :file_metadata, Types::Packages::FileMetadataType, null: true,
description: 'File metadata.'
diff --git a/app/graphql/types/packages/package_tag_type.rb b/app/graphql/types/packages/package_tag_type.rb
index a05ce03da67..450f3fc8e9c 100644
--- a/app/graphql/types/packages/package_tag_type.rb
+++ b/app/graphql/types/packages/package_tag_type.rb
@@ -7,8 +7,8 @@ module Types
description 'Represents a package tag'
authorize :read_package
- field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the tag.'
- field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the tag.'
+ field :id, GraphQL::Types::ID, null: false, description: 'The ID of the tag.'
+ field :name, GraphQL::Types::String, null: false, description: 'The name of the tag.'
field :created_at, Types::TimeType, null: false, description: 'The created date.'
field :updated_at, Types::TimeType, null: false, description: 'The updated date.'
end
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index ee6785e3555..b8654ebd2c6 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -11,10 +11,10 @@ module Types
field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
description: 'ID of the package.'
- field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package.'
+ field :name, GraphQL::Types::String, null: false, description: 'Name of the package.'
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 :version, GraphQL::STRING_TYPE, null: true, description: 'Version string.'
+ field :version, GraphQL::Types::String, null: true, description: 'Version string.'
field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
diff --git a/app/graphql/types/packages/pypi/metadatum_type.rb b/app/graphql/types/packages/pypi/metadatum_type.rb
index 031d3572197..63452d8ab6e 100644
--- a/app/graphql/types/packages/pypi/metadatum_type.rb
+++ b/app/graphql/types/packages/pypi/metadatum_type.rb
@@ -10,7 +10,7 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Pypi::Metadatum], null: false, description: 'ID of the metadatum.'
- field :required_python, GraphQL::STRING_TYPE, null: true, description: 'Required Python version of the Pypi package.'
+ field :required_python, GraphQL::Types::String, null: true, description: 'Required Python version of the Pypi package.'
end
end
end
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
index deb8560bd79..a2cefb872c9 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -24,7 +24,7 @@ module Types
def self.permission_field(name, **kword_args)
kword_args = kword_args.reverse_merge(
name: name,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
description: "Indicates the user can perform `#{name}` on this resource",
null: false)
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 968635f9e6e..af1f1c54ec2 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -8,44 +8,44 @@ module Types
expose_permissions Types::PermissionTypes::Project
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the project.'
- field :full_path, GraphQL::ID_TYPE, null: false,
+ field :full_path, GraphQL::Types::ID, null: false,
description: 'Full path of the project.'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'Path of the project.'
field :sast_ci_configuration, Types::CiConfiguration::Sast::Type, null: true,
calls_gitaly: true,
description: 'SAST CI configuration for the project.'
- field :name_with_namespace, GraphQL::STRING_TYPE, null: false,
+ field :name_with_namespace, GraphQL::Types::String, null: false,
description: 'Full name of the project with its namespace.'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name of the project (without namespace).'
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Short description of the project.'
markdown_field :description_html, null: true
- field :tag_list, GraphQL::STRING_TYPE, null: true,
+ field :tag_list, GraphQL::Types::String, null: true,
deprecated: { reason: 'Use `topics`', milestone: '13.12' },
description: 'List of project topics (not Git tags).'
- field :topics, [GraphQL::STRING_TYPE], null: true,
+ field :topics, [GraphQL::Types::String], null: true,
description: 'List of project topics.'
- field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true,
+ field :ssh_url_to_repo, GraphQL::Types::String, null: true,
description: 'URL to connect to the project via SSH.'
- field :http_url_to_repo, GraphQL::STRING_TYPE, null: true,
+ field :http_url_to_repo, GraphQL::Types::String, null: true,
description: 'URL to connect to the project via HTTPS.'
- field :web_url, GraphQL::STRING_TYPE, null: true,
+ field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the project.'
- field :star_count, GraphQL::INT_TYPE, null: false,
+ field :star_count, GraphQL::Types::Int, null: false,
description: 'Number of times the project has been starred.'
- field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true, # 4 times
+ field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true, # 4 times
description: 'Number of times the project has been forked.'
field :created_at, Types::TimeType, null: true,
@@ -53,26 +53,26 @@ module Types
field :last_activity_at, Types::TimeType, null: true,
description: 'Timestamp of the project last activity.'
- field :archived, GraphQL::BOOLEAN_TYPE, null: true,
+ field :archived, GraphQL::Types::Boolean, null: true,
description: 'Indicates the archived status of the project.'
- field :visibility, GraphQL::STRING_TYPE, null: true,
+ field :visibility, GraphQL::Types::String, null: true,
description: 'Visibility of the project.'
- field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :shared_runners_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if shared runners are enabled for the project.'
- field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :lfs_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the project has Large File Storage (LFS) enabled.'
- field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :merge_requests_ff_only_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.'
- field :service_desk_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :service_desk_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the project has service desk enabled.'
- field :service_desk_address, GraphQL::STRING_TYPE, null: true,
+ field :service_desk_address, GraphQL::Types::String, null: true,
description: 'E-mail address of the service desk.'
- field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'URL to avatar image file of the project.'
{
@@ -82,7 +82,7 @@ module Types
snippets: 'Snippets are',
container_registry: 'Container Registry is'
}.each do |feature, name_string|
- field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true,
+ field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
description: "Indicates if #{name_string} enabled for the current user"
define_method "#{feature}_enabled" do
@@ -90,36 +90,36 @@ module Types
end
end
- field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :jobs_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.'
- field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true,
+ field :public_jobs, GraphQL::Types::Boolean, method: :public_builds, null: true,
description: 'Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts.'
- field :open_issues_count, GraphQL::INT_TYPE, null: true,
+ field :open_issues_count, GraphQL::Types::Int, null: true,
description: 'Number of open issues for the project.'
- field :import_status, GraphQL::STRING_TYPE, null: true,
+ field :import_status, GraphQL::Types::String, null: true,
description: 'Status of import background job of the project.'
- field :jira_import_status, GraphQL::STRING_TYPE, null: true,
+ field :jira_import_status, GraphQL::Types::String, null: true,
description: 'Status of Jira import background job of the project.'
- field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
+ field :only_allow_merge_if_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
description: 'Indicates if merge requests of the project can only be merged with successful jobs.'
- field :allow_merge_on_skipped_pipeline, GraphQL::BOOLEAN_TYPE, null: true,
+ field :allow_merge_on_skipped_pipeline, GraphQL::Types::Boolean, null: true,
description: 'If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs.'
- field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :request_access_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if users can request member access to the project.'
- field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true,
+ field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::Types::Boolean, null: true,
description: 'Indicates if merge requests of the project can only be merged when all the discussions are resolved.'
- field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :printing_merge_request_link_enabled, GraphQL::Types::Boolean, null: true,
description: 'Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line.'
- field :remove_source_branch_after_merge, GraphQL::BOOLEAN_TYPE, null: true,
+ field :remove_source_branch_after_merge, GraphQL::Types::Boolean, null: true,
description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project.'
- field :autoclose_referenced_issues, GraphQL::BOOLEAN_TYPE, null: true,
+ field :autoclose_referenced_issues, GraphQL::Types::Boolean, null: true,
description: 'Indicates if issues referenced by merge requests and commits within the default branch are closed automatically.'
- field :suggestion_commit_message, GraphQL::STRING_TYPE, null: true,
+ field :suggestion_commit_message, GraphQL::Types::String, null: true,
description: 'The commit message used to apply merge request suggestions.'
- field :squash_read_only, GraphQL::BOOLEAN_TYPE, null: false, method: :squash_readonly?,
+ field :squash_read_only, GraphQL::Types::Boolean, null: false, method: :squash_readonly?,
description: 'Indicates if `squashReadOnly` is enabled.'
field :namespace, Types::NamespaceType, null: true,
@@ -318,14 +318,14 @@ module Types
description: 'Container repositories of the project.',
resolver: Resolvers::ContainerRepositoriesResolver
- field :container_repositories_count, GraphQL::INT_TYPE, null: false,
+ field :container_repositories_count, GraphQL::Types::Int, null: false,
description: 'Number of container repositories in the project.'
field :label,
Types::LabelType,
null: true,
description: 'A label available on this project.' do
- argument :title, GraphQL::STRING_TYPE,
+ argument :title, GraphQL::Types::String,
required: true,
description: 'Title of the label.'
end
@@ -354,6 +354,13 @@ module Types
description: 'The CI Job Tokens scope of access.',
resolver: Resolvers::Ci::JobTokenScopeResolver
+ field :timelogs,
+ Types::TimelogType.connection_type, null: true,
+ description: 'Time logged on issues and merge requests in the project.',
+ extras: [:lookahead],
+ complexity: 5,
+ resolver: ::Resolvers::TimelogResolver
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
diff --git a/app/graphql/types/projects/service_type.rb b/app/graphql/types/projects/service_type.rb
index 6f0dcd44cad..4a9e5dcbfe9 100644
--- a/app/graphql/types/projects/service_type.rb
+++ b/app/graphql/types/projects/service_type.rb
@@ -8,9 +8,9 @@ module Types
# TODO: Add all the fields that we want to expose for the project services integrations
# https://gitlab.com/gitlab-org/gitlab/-/issues/213088
- field :type, GraphQL::STRING_TYPE, null: true,
+ field :type, GraphQL::Types::String, null: true,
description: 'Class name of the service.'
- field :active, GraphQL::BOOLEAN_TYPE, null: true,
+ field :active, GraphQL::Types::Boolean, null: true,
description: 'Indicates if the service is active.'
definition_methods do
diff --git a/app/graphql/types/projects/services/jira_project_type.rb b/app/graphql/types/projects/services/jira_project_type.rb
index 90abce2b4c3..957ac91db6b 100644
--- a/app/graphql/types/projects/services/jira_project_type.rb
+++ b/app/graphql/types/projects/services/jira_project_type.rb
@@ -7,12 +7,12 @@ module Types
class JiraProjectType < BaseObject
graphql_name 'JiraProject'
- field :key, GraphQL::STRING_TYPE, null: false,
+ field :key, GraphQL::Types::String, null: false,
description: 'Key of the Jira project.'
- field :project_id, GraphQL::INT_TYPE, null: false,
+ field :project_id, GraphQL::Types::Int, null: false,
description: 'ID of the Jira project.',
method: :id
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the Jira project.'
end
# rubocop:enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/prometheus_alert_type.rb b/app/graphql/types/prometheus_alert_type.rb
index 8e800536675..8327848032a 100644
--- a/app/graphql/types/prometheus_alert_type.rb
+++ b/app/graphql/types/prometheus_alert_type.rb
@@ -9,11 +9,11 @@ module Types
present_using PrometheusAlertPresenter
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the alert condition.'
field :humanized_text,
- GraphQL::STRING_TYPE,
+ GraphQL::Types::String,
null: false,
description: 'The human-readable text of the alert condition.'
end
diff --git a/app/graphql/types/query_complexity_type.rb b/app/graphql/types/query_complexity_type.rb
index 82809fac22f..3f58a15aef7 100644
--- a/app/graphql/types/query_complexity_type.rb
+++ b/app/graphql/types/query_complexity_type.rb
@@ -9,7 +9,7 @@ module Types
alias_method :query, :object
- field :limit, GraphQL::INT_TYPE,
+ field :limit, GraphQL::Types::Int,
null: true,
method: :max_complexity,
see: {
@@ -18,7 +18,7 @@ module Types
},
description: 'GraphQL query complexity limit.'
- field :score, GraphQL::INT_TYPE,
+ field :score, GraphQL::Types::Int,
null: true,
description: 'GraphQL query complexity score.'
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index d2c67aea95c..7e9cd615719 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -129,7 +129,14 @@ module Types
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
+ field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_MAX_COMPLEXITY / 2 + 1
+
+ field :timelogs, Types::TimelogType.connection_type,
+ null: true,
+ description: 'Find timelogs visible to the current user.',
+ extras: [:lookahead],
+ complexity: 5,
+ resolver: ::Resolvers::TimelogResolver
def design_management
DesignManagementObject.new(nil)
diff --git a/app/graphql/types/release_asset_link_shared_input_arguments.rb b/app/graphql/types/release_asset_link_shared_input_arguments.rb
index 4aa247e47cc..37a6cdd55c9 100644
--- a/app/graphql/types/release_asset_link_shared_input_arguments.rb
+++ b/app/graphql/types/release_asset_link_shared_input_arguments.rb
@@ -5,15 +5,15 @@ module Types
extend ActiveSupport::Concern
included do
- argument :name, GraphQL::STRING_TYPE,
+ argument :name, GraphQL::Types::String,
required: true,
description: 'Name of the asset link.'
- argument :url, GraphQL::STRING_TYPE,
+ argument :url, GraphQL::Types::String,
required: true,
description: 'URL of the asset link.'
- argument :direct_asset_path, GraphQL::STRING_TYPE,
+ argument :direct_asset_path, GraphQL::Types::String,
required: false, as: :filepath,
description: 'Relative path for a direct asset link.'
diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb
index 829e7e246db..02961f2f73f 100644
--- a/app/graphql/types/release_asset_link_type.rb
+++ b/app/graphql/types/release_asset_link_type.rb
@@ -7,20 +7,20 @@ module Types
authorize :read_release
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the link.'
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the link.'
- field :url, GraphQL::STRING_TYPE, null: true,
+ field :url, GraphQL::Types::String, null: true,
description: 'URL of the link.'
field :link_type, Types::ReleaseAssetLinkTypeEnum, null: true,
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`.'
- field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
+ field :external, GraphQL::Types::Boolean, null: true, method: :external?,
description: 'Indicates the link points to an external resource.'
- field :direct_asset_url, GraphQL::STRING_TYPE, null: true,
+ field :direct_asset_url, GraphQL::Types::String, null: true,
description: 'Direct asset URL of the link.'
- field :direct_asset_path, GraphQL::STRING_TYPE, null: true, method: :filepath,
+ field :direct_asset_path, GraphQL::Types::String, null: true, method: :filepath,
description: 'Relative path for the direct asset link.'
def direct_asset_url
diff --git a/app/graphql/types/release_assets_type.rb b/app/graphql/types/release_assets_type.rb
index d847d9842d5..ea6ee0b5fd9 100644
--- a/app/graphql/types/release_assets_type.rb
+++ b/app/graphql/types/release_assets_type.rb
@@ -11,7 +11,7 @@ module Types
present_using ReleasePresenter
- field :count, GraphQL::INT_TYPE, null: true, method: :assets_count,
+ field :count, GraphQL::Types::Int, null: true, method: :assets_count,
description: 'Number of assets of the release.'
field :links, Types::ReleaseAssetLinkType.connection_type, null: true, method: :sorted_links,
description: 'Asset links of the release.'
diff --git a/app/graphql/types/release_links_type.rb b/app/graphql/types/release_links_type.rb
index a51b80e1e13..7830e29f3cd 100644
--- a/app/graphql/types/release_links_type.rb
+++ b/app/graphql/types/release_links_type.rb
@@ -10,20 +10,20 @@ module Types
present_using ReleasePresenter
- field :self_url, GraphQL::STRING_TYPE, null: true,
+ field :self_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the release.'
- field :edit_url, GraphQL::STRING_TYPE, null: true,
+ field :edit_url, GraphQL::Types::String, null: true,
description: "HTTP URL of the release's edit page.",
authorize: :update_release
- field :opened_merge_requests_url, GraphQL::STRING_TYPE, null: true,
+ field :opened_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.'
- field :merged_merge_requests_url, GraphQL::STRING_TYPE, null: true,
+ field :merged_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.'
- field :closed_merge_requests_url, GraphQL::STRING_TYPE, null: true,
+ field :closed_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.'
- field :opened_issues_url, GraphQL::STRING_TYPE, null: true,
+ field :opened_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.'
- field :closed_issues_url, GraphQL::STRING_TYPE, null: true,
+ field :closed_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.'
end
end
diff --git a/app/graphql/types/release_source_type.rb b/app/graphql/types/release_source_type.rb
index 10fc202514d..fd29a69d72a 100644
--- a/app/graphql/types/release_source_type.rb
+++ b/app/graphql/types/release_source_type.rb
@@ -7,9 +7,9 @@ module Types
authorize :download_code
- field :format, GraphQL::STRING_TYPE, null: true,
+ field :format, GraphQL::Types::String, null: true,
description: 'Format of the source.'
- field :url, GraphQL::STRING_TYPE, null: true,
+ field :url, GraphQL::Types::String, null: true,
description: 'Download URL of the source.'
end
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index 81813a10a3e..5e8f00b2b0a 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -13,22 +13,22 @@ module Types
present_using ReleasePresenter
- field :tag_name, GraphQL::STRING_TYPE, null: true, method: :tag,
+ field :tag_name, GraphQL::Types::String, null: true, method: :tag,
description: 'Name of the tag associated with the release.',
authorize: :download_code
- field :tag_path, GraphQL::STRING_TYPE, null: true,
+ field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code
- field :description, GraphQL::STRING_TYPE, null: true,
+ field :description, GraphQL::Types::String, null: true,
description: 'Description (also known as "release notes") of the release.'
markdown_field :description_html, null: true
- field :name, GraphQL::STRING_TYPE, null: true,
+ field :name, GraphQL::Types::String, null: true,
description: 'Name of the release.'
field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was created.'
field :released_at, Types::TimeType, null: true,
description: 'Timestamp of when the release was released.'
- field :upcoming_release, GraphQL::BOOLEAN_TYPE, null: true, method: :upcoming_release?,
+ field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?,
description: 'Indicates the release is an upcoming release.'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release.'
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index 8ed97d7e663..b6a1a91fd7a 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -8,67 +8,67 @@ module Types
graphql_name 'RepositoryBlob'
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the blob.'
- field :oid, GraphQL::STRING_TYPE, null: false, method: :id,
+ field :oid, GraphQL::Types::String, null: false, method: :id,
description: 'OID of the blob.'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'Path of the blob.'
- field :name, GraphQL::STRING_TYPE,
+ field :name, GraphQL::Types::String,
description: 'Blob name.',
null: true
- field :mode, type: GraphQL::STRING_TYPE,
+ field :mode, type: GraphQL::Types::String,
description: 'Blob mode.',
null: true
- field :lfs_oid, GraphQL::STRING_TYPE, null: true,
+ field :lfs_oid, GraphQL::Types::String, null: true,
calls_gitaly: true,
description: 'LFS OID of the blob.'
- field :web_path, GraphQL::STRING_TYPE, null: true,
+ field :web_path, GraphQL::Types::String, null: true,
description: 'Web path of the blob.'
- field :ide_edit_path, GraphQL::STRING_TYPE, null: true,
+ field :ide_edit_path, GraphQL::Types::String, null: true,
description: 'Web path to edit this blob in the Web IDE.'
- field :fork_and_edit_path, GraphQL::STRING_TYPE, null: true,
+ field :fork_and_edit_path, GraphQL::Types::String, null: true,
description: 'Web path to edit this blob using a forked project.'
- field :ide_fork_and_edit_path, GraphQL::STRING_TYPE, null: true,
+ field :ide_fork_and_edit_path, GraphQL::Types::String, null: true,
description: 'Web path to edit this blob in the Web IDE using a forked project.'
- field :size, GraphQL::INT_TYPE, null: true,
+ field :size, GraphQL::Types::Int, null: true,
description: 'Size (in bytes) of the blob.'
- field :raw_size, GraphQL::INT_TYPE, null: true,
+ field :raw_size, GraphQL::Types::Int, 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,
+ field :raw_blob, GraphQL::Types::String, null: true, method: :data,
description: 'The raw content of the blob.'
- field :raw_text_blob, GraphQL::STRING_TYPE, null: true, method: :text_only_data,
+ field :raw_text_blob, GraphQL::Types::String, 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?,
+ field :stored_externally, GraphQL::Types::Boolean, 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,
+ field :edit_blob_path, GraphQL::Types::String, null: true,
description: 'Web path to edit the blob in the old-style editor.'
- field :raw_path, GraphQL::STRING_TYPE, null: true,
+ field :raw_path, GraphQL::Types::String, null: true,
description: 'Web path to download the raw blob.'
- field :external_storage_url, GraphQL::STRING_TYPE, null: true,
+ field :external_storage_url, GraphQL::Types::String, null: true,
description: 'Web path to download the raw blob via external storage, if enabled.'
- field :replace_path, GraphQL::STRING_TYPE, null: true,
+ field :replace_path, GraphQL::Types::String, null: true,
description: 'Web path to replace the blob content.'
- field :file_type, GraphQL::STRING_TYPE, null: true,
+ field :file_type, GraphQL::Types::String, null: true,
description: 'The expected format of the blob based on the extension.'
field :simple_viewer, type: Types::BlobViewerType,
@@ -79,12 +79,12 @@ module Types
description: 'Blob content rich viewer.',
null: true
- field :plain_data, GraphQL::STRING_TYPE,
+ field :plain_data, GraphQL::Types::String,
description: 'Blob plain highlighted data.',
null: true,
calls_gitaly: true
- field :can_modify_blob, GraphQL::BOOLEAN_TYPE, null: true, method: :can_modify_blob?,
+ field :can_modify_blob, GraphQL::Types::Boolean, null: true, method: :can_modify_blob?,
calls_gitaly: true,
description: 'Whether the current user can modify the blob.'
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 9d896888fa7..63d1eef5b59 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -6,20 +6,24 @@ module Types
authorize :download_code
- field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ field :root_ref, GraphQL::Types::String, null: true, calls_gitaly: true,
description: 'Default branch of the repository.'
- field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true,
+ field :empty, GraphQL::Types::Boolean, null: false, method: :empty?, calls_gitaly: true,
description: 'Indicates repository has no visible content.'
- field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists?, calls_gitaly: true,
+ field :exists, GraphQL::Types::Boolean, null: false, method: :exists?, calls_gitaly: true,
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
+ field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
+ max_page_size: 100,
+ description: 'Paginated tree of the repository.',
+ feature_flag: :paginated_tree_graphql_query
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
- field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
+ field :branch_names, [GraphQL::Types::String], 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,
+ field :disk_path, GraphQL::Types::String,
description: 'Shows a disk path of the repository.',
null: true,
authorize: :read_storage_disk_path
diff --git a/app/graphql/types/resolvable_interface.rb b/app/graphql/types/resolvable_interface.rb
index a9d745c2bc1..42784aa5e00 100644
--- a/app/graphql/types/resolvable_interface.rb
+++ b/app/graphql/types/resolvable_interface.rb
@@ -16,10 +16,10 @@ module Types
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.resolved_by_id).find
end
- field :resolved, GraphQL::BOOLEAN_TYPE, null: false,
+ field :resolved, GraphQL::Types::Boolean, null: false,
description: 'Indicates if the object is resolved.',
method: :resolved?
- field :resolvable, GraphQL::BOOLEAN_TYPE, null: false,
+ field :resolvable, GraphQL::Types::Boolean, null: false,
description: 'Indicates if the object can be resolved.',
method: :resolvable?
field :resolved_at, Types::TimeType, null: true,
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index 7606bdbc46d..c345aea08bd 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -17,7 +17,7 @@ module Types
description: 'ID of the snippet.',
null: false
- field :title, GraphQL::STRING_TYPE,
+ field :title, GraphQL::Types::String,
description: 'Title of the snippet.',
null: false
@@ -33,11 +33,11 @@ module Types
description: 'The owner of the snippet.',
null: true
- field :file_name, GraphQL::STRING_TYPE,
+ field :file_name, GraphQL::Types::String,
description: 'File Name of the snippet.',
null: true
- field :description, GraphQL::STRING_TYPE,
+ field :description, GraphQL::Types::String,
description: 'Description of the snippet.',
null: true
@@ -53,11 +53,11 @@ module Types
description: 'Timestamp this snippet was updated.',
null: false
- field :web_url, type: GraphQL::STRING_TYPE,
+ field :web_url, type: GraphQL::Types::String,
description: 'Web URL of the snippet.',
null: false
- field :raw_url, type: GraphQL::STRING_TYPE,
+ field :raw_url, type: GraphQL::Types::String,
description: 'Raw URL of the snippet.',
null: false
@@ -67,12 +67,12 @@ module Types
null: true,
resolver: Resolvers::Snippets::BlobsResolver
- field :ssh_url_to_repo, type: GraphQL::STRING_TYPE,
+ field :ssh_url_to_repo, type: GraphQL::Types::String,
description: 'SSH URL to the snippet repository.',
calls_gitaly: true,
null: true
- field :http_url_to_repo, type: GraphQL::STRING_TYPE,
+ field :http_url_to_repo, type: GraphQL::Types::String,
description: 'HTTP URL to the snippet repository.',
calls_gitaly: true,
null: true
diff --git a/app/graphql/types/snippets/blob_action_input_type.rb b/app/graphql/types/snippets/blob_action_input_type.rb
index 13eade3dcc4..45dc4be8451 100644
--- a/app/graphql/types/snippets/blob_action_input_type.rb
+++ b/app/graphql/types/snippets/blob_action_input_type.rb
@@ -10,15 +10,15 @@ module Types
description: 'Type of input action.',
required: true
- argument :previous_path, GraphQL::STRING_TYPE,
+ argument :previous_path, GraphQL::Types::String,
description: 'Previous path of the snippet file.',
required: false
- argument :file_path, GraphQL::STRING_TYPE,
+ argument :file_path, GraphQL::Types::String,
description: 'Path of the snippet file.',
required: true
- argument :content, GraphQL::STRING_TYPE,
+ argument :content, GraphQL::Types::String,
description: 'Snippet file content.',
required: false
end
diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb
index 1335838935e..d5da271d936 100644
--- a/app/graphql/types/snippets/blob_type.rb
+++ b/app/graphql/types/snippets/blob_type.rb
@@ -8,36 +8,36 @@ module Types
description 'Represents the snippet blob'
present_using SnippetBlobPresenter
- field :rich_data, GraphQL::STRING_TYPE,
+ field :rich_data, GraphQL::Types::String,
description: 'Blob highlighted data.',
null: true
- field :plain_data, GraphQL::STRING_TYPE,
+ field :plain_data, GraphQL::Types::String,
description: 'Blob plain highlighted data.',
null: true
- field :raw_plain_data, GraphQL::STRING_TYPE,
+ field :raw_plain_data, GraphQL::Types::String,
description: 'The raw content of the blob, if the blob is text data.',
null: true
- field :raw_path, GraphQL::STRING_TYPE,
+ field :raw_path, GraphQL::Types::String,
description: 'Blob raw content endpoint path.',
null: false
- field :size, GraphQL::INT_TYPE,
+ field :size, GraphQL::Types::Int,
description: 'Blob size.',
null: false
- field :binary, GraphQL::BOOLEAN_TYPE,
+ field :binary, GraphQL::Types::Boolean,
description: 'Shows whether the blob is binary.',
method: :binary?,
null: false
- field :name, GraphQL::STRING_TYPE,
+ field :name, GraphQL::Types::String,
description: 'Blob name.',
null: true
- field :path, GraphQL::STRING_TYPE,
+ field :path, GraphQL::Types::String,
description: 'Blob path.',
null: true
@@ -49,15 +49,15 @@ module Types
description: 'Blob content rich viewer.',
null: true
- field :mode, type: GraphQL::STRING_TYPE,
+ field :mode, type: GraphQL::Types::String,
description: 'Blob mode.',
null: true
- field :external_storage, type: GraphQL::STRING_TYPE,
+ field :external_storage, type: GraphQL::Types::String,
description: 'Blob external storage.',
null: true
- field :rendered_as_text, type: GraphQL::BOOLEAN_TYPE,
+ field :rendered_as_text, type: GraphQL::Types::Boolean,
description: 'Shows whether the blob is rendered as text.',
method: :rendered_as_text?,
null: false
diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb
index 6837256f202..3aa19ff9413 100644
--- a/app/graphql/types/task_completion_status.rb
+++ b/app/graphql/types/task_completion_status.rb
@@ -8,9 +8,9 @@ module Types
graphql_name 'TaskCompletionStatus'
description 'Completion status of tasks'
- field :count, GraphQL::INT_TYPE, null: false,
+ field :count, GraphQL::Types::Int, null: false,
description: 'Number of total tasks.'
- field :completed_count, GraphQL::INT_TYPE, null: false,
+ field :completed_count, GraphQL::Types::Int, null: false,
description: 'Number of completed tasks.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb
index 9e2c47a9ece..cbd5aeaeef9 100644
--- a/app/graphql/types/terraform/state_type.rb
+++ b/app/graphql/types/terraform/state_type.rb
@@ -9,11 +9,11 @@ module Types
connection_type_class(Types::CountableConnectionType)
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
null: false,
description: 'ID of the Terraform state.'
- field :name, GraphQL::STRING_TYPE,
+ field :name, GraphQL::Types::String,
null: false,
description: 'Name of the Terraform state.'
diff --git a/app/graphql/types/terraform/state_version_type.rb b/app/graphql/types/terraform/state_version_type.rb
index 2cd2ec8dcda..545b3c0044d 100644
--- a/app/graphql/types/terraform/state_version_type.rb
+++ b/app/graphql/types/terraform/state_version_type.rb
@@ -9,7 +9,7 @@ module Types
authorize :read_terraform_state
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
null: false,
description: 'ID of the Terraform state version.'
@@ -17,7 +17,7 @@ module Types
null: true,
description: 'The user that created this version.'
- field :download_path, GraphQL::STRING_TYPE,
+ field :download_path, GraphQL::Types::String,
null: true,
description: "URL for downloading the version's JSON file."
@@ -25,7 +25,7 @@ module Types
null: true,
description: 'The job that created this version.'
- field :serial, GraphQL::INT_TYPE,
+ field :serial, GraphQL::Types::Int,
null: true,
description: 'Serial number of the version.',
method: :version
diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb
index 925a522629e..206aabbada3 100644
--- a/app/graphql/types/timelog_type.rb
+++ b/app/graphql/types/timelog_type.rb
@@ -12,7 +12,7 @@ module Types
description: 'Timestamp of when the time tracked was spent at.'
field :time_spent,
- GraphQL::INT_TYPE,
+ GraphQL::Types::Int,
null: false,
description: 'The time spent displayed in seconds.'
@@ -36,6 +36,10 @@ module Types
null: true,
description: 'The note where the quick action to add the logged time was executed.'
+ field :summary, GraphQL::Types::String,
+ null: true,
+ description: 'The summary of how the time was spent.'
+
def user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find
end
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 3b983060de2..24c110ce09b 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -9,7 +9,7 @@ module Types
authorize :read_todo
- field :id, GraphQL::ID_TYPE,
+ field :id, GraphQL::Types::ID,
description: 'ID of the to-do item.',
null: false
@@ -35,7 +35,7 @@ module Types
description: 'Target type of the to-do item.',
null: false
- field :body, GraphQL::STRING_TYPE,
+ field :body, GraphQL::Types::String,
description: 'Body of the to-do item.',
null: false,
calls_gitaly: true # TODO This is only true when `target_type` is `Commit`. See https://gitlab.com/gitlab-org/gitlab/issues/34757#note_234752665
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index d192c8d3c57..bb15d91a62f 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -10,14 +10,14 @@ module Types
graphql_name 'Blob'
- field :web_url, GraphQL::STRING_TYPE, null: true,
+ field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL of the blob.'
- field :web_path, GraphQL::STRING_TYPE, null: true,
+ field :web_path, GraphQL::Types::String, null: true,
description: 'Web path of the blob.'
- field :lfs_oid, GraphQL::STRING_TYPE, null: true,
+ field :lfs_oid, GraphQL::Types::String, null: true,
calls_gitaly: true,
description: 'LFS ID of the blob.'
- field :mode, GraphQL::STRING_TYPE, null: true,
+ field :mode, GraphQL::Types::String, null: true,
description: 'Blob mode in numeric format.'
def lfs_oid
diff --git a/app/graphql/types/tree/entry_type.rb b/app/graphql/types/tree/entry_type.rb
index c0150b77c55..1c612f91a5b 100644
--- a/app/graphql/types/tree/entry_type.rb
+++ b/app/graphql/types/tree/entry_type.rb
@@ -4,17 +4,17 @@ module Types
module EntryType
include Types::BaseInterface
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id, GraphQL::Types::ID, null: false,
description: 'ID of the entry.'
- field :sha, GraphQL::STRING_TYPE, null: false,
+ field :sha, GraphQL::Types::String, null: false,
description: 'Last commit SHA for the entry.', method: :id
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name, GraphQL::Types::String, null: false,
description: 'Name of the entry.'
field :type, Tree::TypeEnum, null: false,
description: 'Type of tree entry.'
- field :path, GraphQL::STRING_TYPE, null: false,
+ field :path, GraphQL::Types::String, null: false,
description: 'Path of the entry.'
- field :flat_path, GraphQL::STRING_TYPE, null: false,
+ field :flat_path, GraphQL::Types::String, null: false,
description: 'Flat path of the entry.'
end
end
diff --git a/app/graphql/types/tree/submodule_type.rb b/app/graphql/types/tree/submodule_type.rb
index 519e866ebb0..05d8c1a951a 100644
--- a/app/graphql/types/tree/submodule_type.rb
+++ b/app/graphql/types/tree/submodule_type.rb
@@ -8,9 +8,9 @@ module Types
graphql_name 'Submodule'
- field :web_url, type: GraphQL::STRING_TYPE, null: true,
+ field :web_url, type: GraphQL::Types::String, null: true,
description: 'Web URL for the sub-module.'
- field :tree_url, type: GraphQL::STRING_TYPE, null: true,
+ field :tree_url, type: GraphQL::Types::String, null: true,
description: 'Tree URL for the sub-module.'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index daf4b5421fb..998b3617574 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -11,9 +11,9 @@ module Types
graphql_name 'TreeEntry'
description 'Represents a directory'
- field :web_url, GraphQL::STRING_TYPE, null: true,
+ field :web_url, GraphQL::Types::String, null: true,
description: 'Web URL for the tree entry (directory).'
- field :web_path, GraphQL::STRING_TYPE, null: true,
+ field :web_path, GraphQL::Types::String, null: true,
description: 'Web path for the tree entry (directory).'
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 7d61b296eae..71c6b7f3019 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -14,20 +14,20 @@ module Types
method: :itself
field :id,
- type: GraphQL::ID_TYPE,
+ type: GraphQL::Types::ID,
null: false,
description: 'ID of the user.'
field :bot,
- type: GraphQL::BOOLEAN_TYPE,
+ type: GraphQL::Types::Boolean,
null: false,
description: 'Indicates if the user is a bot.',
method: :bot?
field :username,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: false,
description: 'Username of the user. Unique within this instance of GitLab.'
field :name,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: false,
description: 'Human-readable name of the user.'
field :state,
@@ -35,24 +35,24 @@ module Types
null: false,
description: 'State of the user.'
field :email,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
description: 'User email.', method: :public_email,
deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' }
field :public_email,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
description: "User's public email."
field :avatar_url,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: true,
description: "URL of the user's avatar."
field :web_url,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: false,
description: 'Web URL of the user.'
field :web_path,
- type: GraphQL::STRING_TYPE,
+ type: GraphQL::Types::String,
null: false,
description: 'Web path of the user.'
field :group_memberships,
@@ -67,7 +67,7 @@ module Types
null: true,
description: 'User status.'
field :location,
- type: ::GraphQL::STRING_TYPE,
+ type: ::GraphQL::Types::String,
null: true,
description: 'The location of the user.'
field :project_memberships,
@@ -77,6 +77,10 @@ module Types
field :starred_projects,
description: 'Projects starred by the user.',
resolver: Resolvers::UserStarredProjectsResolver
+ field :namespace,
+ type: Types::NamespaceType,
+ null: true,
+ description: 'Personal namespace of the user.'
field :todos, resolver: Resolvers::TodoResolver, description: 'To-do items of the user.' do
extension(::Gitlab::Graphql::TodosProjectPermissionPreloader::FieldExtension)
@@ -100,6 +104,13 @@ module Types
Types::UserCalloutType.connection_type,
null: true,
description: 'User callouts that belong to the user.'
+ field :timelogs,
+ Types::TimelogType.connection_type,
+ null: true,
+ description: 'Time logged by the user.',
+ extras: [:lookahead],
+ complexity: 5,
+ resolver: ::Resolvers::TimelogResolver
definition_methods do
def resolve_type(object, context)
diff --git a/app/graphql/types/user_merge_request_interaction_type.rb b/app/graphql/types/user_merge_request_interaction_type.rb
index b9ff489e0d6..ff6e83efbb2 100644
--- a/app/graphql/types/user_merge_request_interaction_type.rb
+++ b/app/graphql/types/user_merge_request_interaction_type.rb
@@ -13,14 +13,14 @@ module Types
authorize :read_merge_request
field :can_merge,
- type: ::GraphQL::BOOLEAN_TYPE,
+ type: ::GraphQL::Types::Boolean,
null: false,
calls_gitaly: true,
method: :can_merge?,
description: 'Whether this user can merge this merge request.'
field :can_update,
- type: ::GraphQL::BOOLEAN_TYPE,
+ type: ::GraphQL::Types::Boolean,
null: false,
method: :can_update?,
description: 'Whether this user can update this merge request.'
@@ -31,13 +31,13 @@ module Types
description: 'The state of the review by this user.'
field :reviewed,
- type: ::GraphQL::BOOLEAN_TYPE,
+ type: ::GraphQL::Types::Boolean,
null: false,
method: :reviewed?,
description: 'Whether this user has provided a review for this merge request.'
field :approved,
- type: ::GraphQL::BOOLEAN_TYPE,
+ type: ::GraphQL::Types::Boolean,
null: false,
method: :approved?,
description: 'Whether this user has approved this merge request.'
diff --git a/app/graphql/types/user_status_type.rb b/app/graphql/types/user_status_type.rb
index c1a736a3722..61abec0ba96 100644
--- a/app/graphql/types/user_status_type.rb
+++ b/app/graphql/types/user_status_type.rb
@@ -7,9 +7,9 @@ module Types
markdown_field :message_html, null: true,
description: 'HTML of the user status message'
- field :message, GraphQL::STRING_TYPE, null: true,
+ field :message, GraphQL::Types::String, null: true,
description: 'User status message.'
- field :emoji, GraphQL::STRING_TYPE, null: true,
+ field :emoji, GraphQL::Types::String, null: true,
description: 'String representation of emoji.'
field :availability, Types::AvailabilityEnum, null: false,
description: 'User availability status.'
diff --git a/app/helpers/admin/user_actions_helper.rb b/app/helpers/admin/user_actions_helper.rb
index 5719d8f5ffd..dc31c06477e 100644
--- a/app/helpers/admin/user_actions_helper.rb
+++ b/app/helpers/admin/user_actions_helper.rb
@@ -48,9 +48,9 @@ module Admin
end
def delete_actions
- return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval? && @user.can_be_removed?
+ return unless can?(current_user, :destroy_user, @user) && !@user.blocked_pending_approval?
- @actions << 'delete'
+ @actions << 'delete' if @user.can_be_removed?
@actions << 'delete_with_contributions'
end
diff --git a/app/helpers/analytics/navbar_helper.rb b/app/helpers/analytics/navbar_helper.rb
deleted file mode 100644
index 091571ff15a..00000000000
--- a/app/helpers/analytics/navbar_helper.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Analytics
- module NavbarHelper
- class NavbarSubItem
- attr_reader :title, :path, :link, :link_to_options
-
- def initialize(title:, path:, link:, link_to_options: {})
- @title = title
- @path = path
- @link = link
- @link_to_options = link_to_options.merge(title: title)
- end
- end
-
- def group_analytics_navbar_links(group, current_user)
- []
- end
-
- private
-
- def navbar_sub_item(args)
- NavbarSubItem.new(**args)
- end
- end
-end
-
-Analytics::NavbarHelper.prepend_mod_with('Analytics::NavbarHelper')
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 1304bcb1c7e..58f933a7fe0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -413,6 +413,12 @@ module ApplicationHelper
end
end
+ def gitlab_ui_form_for(record, *args, &block)
+ options = args.extract_options!
+
+ form_for(record, *(args << options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder })), &block)
+ end
+
private
def appearance
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index a3df566e4b3..2447a731167 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -447,6 +447,12 @@ module ApplicationSettingsHelper
def signup_enabled?
!!Gitlab::CurrentSettings.signup_enabled
end
+
+ def pending_user_count
+ return 0 if Gitlab::CurrentSettings.new_user_signups_cap.blank?
+
+ User.blocked_pending_approval.count
+ end
end
ApplicationSettingsHelper.prepend_mod_with('ApplicationSettingsHelper')
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index f72f8bfd151..33d5bae88f4 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -119,14 +119,6 @@ module BoardsHelper
}
end
- def boards_link_text
- if current_board_parent.multiple_issue_boards_available?
- s_("IssueBoards|Boards")
- else
- s_("IssueBoards|Board")
- end
- end
-
def recent_boards_path
recent_project_boards_path(@project) if current_board_parent.is_a?(Project)
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 27d6ee57d8b..eb30ffc0093 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -22,7 +22,7 @@ module ButtonHelper
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || _('Copy')
- button_text = data[:button_text] || ''
+ button_text = data[:button_text] || nil
hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false
item_prop = data[:itemprop] || nil
@@ -55,8 +55,8 @@ module ButtonHelper
}
content_tag :button, button_attributes do
- concat(sprite_icon('copy-to-clipboard')) unless hide_button_icon
- concat(button_text)
+ concat(sprite_icon('copy-to-clipboard', css_class: ['gl-icon', *('gl-button-icon' unless button_text.nil?)].join(' '))) unless hide_button_icon
+ concat(content_tag(:span, button_text, class: 'gl-button-text')) unless button_text.nil?
end
end
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index d441ffbb853..4dfe136c206 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -21,7 +21,7 @@ module Ci
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
"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'),
+ "needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
"pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
"pipeline-page-path" => project_pipelines_path(project),
@@ -30,7 +30,7 @@ module Ci
"project-namespace" => project.namespace.full_path,
"runner-help-page-path" => help_page_path('ci/runners/index'),
"total-branches" => project.repository.branches.length,
- "yml-help-page-path" => help_page_path('ci/yaml/README')
+ "yml-help-page-path" => help_page_path('ci/yaml/index')
}
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index e9a75babb97..c355fa5cc67 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -24,7 +24,8 @@ module ClustersHelper
agent_docs_url: help_page_path('user/clusters/agent/index'),
install_docs_url: help_page_path('administration/clusters/kas'),
get_started_docs_url: help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'),
- integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent')
+ integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'),
+ kas_address: Gitlab::Kas.external_url
}
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d2ac1e8f985..53017beee85 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -215,7 +215,7 @@ module CommitsHelper
path = project_blob_path(project, tree_join(commit_sha, diff_new_path))
title = replaced ? _('View replaced file @ ') : _('View file @ ')
- link_to(path, class: 'btn gl-button btn-default') do
+ link_to(path, class: 'btn gl-button btn-default gl-ml-3') do
raw(title) + content_tag(:span, truncate_sha(commit_sha), class: 'commit-sha')
end
end
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index b07baf59114..f9d62747308 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -38,10 +38,8 @@ module CompareHelper
project_merge_request_path: merge_request.present? ? project_merge_request_path(project, merge_request) : '',
create_mr_path: create_mr_button? ? create_mr_path : ''
}.tap do |data|
- if Feature.enabled?(:compare_repo_dropdown, project, default_enabled: :yaml)
- data[:project_to] = { id: project.id, name: project.full_path }.to_json
- data[:projects_from] = target_projects(project).map { |project| { id: project.id, name: project.full_path } }.to_json
- end
+ data[:project_to] = { id: project.id, name: project.full_path }.to_json
+ data[:projects_from] = target_projects(project).map { |project| { id: project.id, name: project.full_path } }.to_json
end
end
end
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 95bbf2eff41..f0e1f252917 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -23,10 +23,6 @@ module DashboardHelper
dashboard_nav_links.include?(link)
end
- def any_dashboard_nav_link?(links)
- links.any? { |link| dashboard_nav_link?(link) }
- end
-
def has_start_trial?
false
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 3aa54e3afe9..40d7eab584c 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -273,14 +273,14 @@ module DiffHelper
Gitlab::CodeNavigationPath.new(merge_request.project, merge_request.diff_head_sha)
end
- def conflicts
+ def conflicts(allow_tree_conflicts: false)
return unless options[:merge_ref_head_diff]
- conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request) # rubocop:disable CodeReuse/ServiceClass
+ conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
- return unless conflicts_service.can_be_resolved_in_ui?
+ return unless allow_tree_conflicts || conflicts_service.can_be_resolved_in_ui?
- conflicts_service.conflicts.files.index_by(&:our_path)
+ conflicts_service.conflicts.files.index_by(&:path)
end
def log_overflow_limits(diff_files:, collection_overflow:)
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 0b1bdb68e50..b804efb9561 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -7,21 +7,9 @@ module EmailsHelper
# https://developers.google.com/gmail/markup/reference/go-to-action
def email_action(url)
name = action_title(url)
- if name
- data = {
- "@context" => "http://schema.org",
- "@type" => "EmailMessage",
- "action" => {
- "@type" => "ViewAction",
- "name" => name,
- "url" => url
- }
- }
-
- content_tag :script, type: 'application/ld+json' do
- data.to_json.html_safe
- end
- end
+ return unless name
+
+ gmail_goto_action(name, url)
end
def action_title(url)
@@ -36,6 +24,22 @@ module EmailsHelper
nil
end
+ def gmail_goto_action(name, url)
+ data = {
+ "@context" => "http://schema.org",
+ "@type" => "EmailMessage",
+ "action" => {
+ "@type" => "ViewAction",
+ "name" => name,
+ "url" => url
+ }
+ }
+
+ content_tag :script, type: 'application/ld+json' do
+ data.to_json.html_safe
+ end
+ end
+
def sanitize_name(name)
if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF]
name.tr('.', '_')
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 491d2731e91..3f23f73eed7 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -65,4 +65,31 @@ module EnvironmentHelper
content_tag(:span, text, class: klass)
end
end
+
+ def environments_detail_data(user, project, environment)
+ {
+ name: environment.name,
+ id: environment.id,
+ external_url: environment.external_url,
+ can_update_environment: can?(current_user, :update_environment, environment),
+ can_destroy_environment: can_destroy_environment?(environment),
+ can_read_environment: can?(current_user, :read_environment, environment),
+ can_stop_environment: can?(current_user, :stop_environment, environment),
+ can_admin_environment: can?(current_user, :admin_environment, project),
+ environment_metrics_path: environment_metrics_path(environment),
+ environments_fetch_path: project_environments_path(project, format: :json),
+ environment_edit_path: edit_project_environment_path(project, environment),
+ environment_stop_path: stop_project_environment_path(project, environment),
+ environment_delete_path: environment_delete_path(environment),
+ environment_cancel_auto_stop_path: cancel_auto_stop_project_environment_path(project, environment),
+ environment_terminal_path: terminal_project_environment_path(project, environment),
+ has_terminals: environment.has_terminals?,
+ is_environment_available: environment.available?,
+ auto_stop_at: environment.auto_stop_at
+ }
+ end
+
+ def environments_detail_data_json(user, project, environment)
+ environments_detail_data(user, project, environment).to_json
+ end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 62060200698..b2842664879 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -45,6 +45,14 @@ module EnvironmentsHelper
can?(current_user, :destroy_environment, environment)
end
+ def environment_data(environment)
+ Gitlab::Json.generate({
+ id: environment.id,
+ name: environment.name,
+ external_url: environment.external_url
+ })
+ end
+
private
def project_metrics_data(project)
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index c4d920dc317..07ab246b089 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -13,7 +13,7 @@ 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_members_app_data_json(group, members:, invited:, access_requests:)
+ def group_members_app_data(group, members:, invited:, access_requests:)
{
user: group_members_list_data(group, members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }),
group: group_group_links_list_data(group),
@@ -21,7 +21,7 @@ module Groups::GroupMembersHelper
access_request: group_members_list_data(group, access_requests.nil? ? [] : access_requests),
source_id: group.id,
can_manage_members: can?(current_user, :admin_group_member, group)
- }.to_json
+ }
end
private
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 400ad721b06..0e4aeaae20d 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,52 +1,6 @@
# frozen_string_literal: true
module GroupsHelper
- def group_overview_nav_link_paths
- %w[
- groups#activity
- groups#subgroups
- labels#index
- group_members#index
- ]
- end
-
- def group_settings_nav_link_paths
- %w[
- groups#projects
- groups#edit
- badges#index
- repository#show
- ci_cd#show
- integrations#index
- integrations#edit
- ldap_group_links#index
- hooks#index
- pipeline_quota#index
- applications#index
- applications#show
- applications#edit
- packages_and_registries#show
- groups/runners#show
- groups/runners#edit
- ]
- end
-
- def group_packages_nav_link_paths
- %w[
- groups/packages#index
- groups/container_registries#index
- ]
- end
-
- def group_information_title(group)
- group.subgroup? ? _('Subgroup information') : _('Group information')
- end
-
- def group_container_registry_nav?
- Gitlab.config.registry.enabled &&
- can?(current_user, :read_container_image, @group)
- end
-
def group_sidebar_links
@group_sidebar_links ||= get_group_sidebar_links
end
@@ -75,6 +29,10 @@ module GroupsHelper
can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled?
end
+ def can_admin_group_member?(group)
+ Ability.allowed?(current_user, :admin_group_member, group)
+ end
+
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
@@ -89,18 +47,13 @@ module GroupsHelper
.count
end
- def cached_issuables_count(group, type: nil)
- count_service = issuables_count_service_class(type)
- return unless count_service.present?
-
- issuables_count = count_service.new(group, current_user).count
- format_issuables_count(count_service, issuables_count)
- end
-
- def group_dependency_proxy_url(group)
+ def group_dependency_proxy_image_prefix(group)
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
- "#{group_url(group).downcase}#{DependencyProxy::URL_SUFFIX}"
+ url = "#{group_url(group).downcase}#{DependencyProxy::URL_SUFFIX}"
+
+ # Docker images do not include the protocol
+ url.partition('//').last
end
def group_icon_url(group, options = {})
@@ -164,7 +117,7 @@ module GroupsHelper
end
def remove_group_message(group)
- _("You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
+ _("You are going to remove %{group_name}. This will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end
@@ -200,19 +153,6 @@ module GroupsHelper
groups.to_json
end
- def group_packages_nav?
- group_packages_list_nav? ||
- group_container_registry_nav?
- end
-
- def group_dependency_proxy_nav?
- @group.dependency_proxy_feature_available?
- end
-
- def group_packages_list_nav?
- @group.packages_feature_enabled?
- end
-
def show_invite_banner?(group)
can?(current_user, :admin_group, group) &&
!just_created? &&
@@ -250,6 +190,12 @@ module GroupsHelper
can?(current_user, "read_group_#{resource}".to_sym, @group)
end
+ # TODO Proper policies, such as `read_group_runners, should be implemented per
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
+ if can?(current_user, :admin_group, @group) && Feature.enabled?(:runner_list_group_view_vue_ui, @group, default_enabled: :yaml)
+ links << :runners
+ end
+
if can?(current_user, :read_cluster, @group)
links << :kubernetes
end
@@ -322,24 +268,8 @@ module GroupsHelper
s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
end
- def issuables_count_service_class(type)
- if type == :issues
- Groups::OpenIssuesCountService
- elsif type == :merge_requests
- Groups::MergeRequestsCountService
- end
- end
-
- def format_issuables_count(count_service, count)
- if count > count_service::CACHED_COUNT_THRESHOLD
- ActiveSupport::NumberHelper
- .number_to_human(
- count,
- units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
- )
- else
- number_with_delimiter(count)
- end
+ def group_url_error_message
+ s_('GroupSettings|Please choose a group URL with no special characters or spaces.')
end
end
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index b92e418006b..41c7abbbabd 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -8,7 +8,7 @@ module IdeHelper
'committed-state-svg-path' => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
'pipelines-empty-state-svg-path': image_path('illustrations/pipelines_empty.svg'),
'promotion-svg-path': image_path('illustrations/web-ide_promotion.svg'),
- 'ci-help-page-path' => help_page_path('ci/quick_start/README'),
+ 'ci-help-page-path' => help_page_path('ci/quick_start/index'),
'web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md'),
'clientside-preview-enabled': Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s,
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index ab305d822e8..f15566a551a 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -47,6 +47,10 @@ module IntegrationsHelper
end
end
+ def scoped_overrides_integration_path(integration, options = {})
+ overrides_admin_application_settings_integration_path(integration, options)
+ end
+
def scoped_test_integration_path(integration)
if @project.present?
test_project_service_path(@project, integration)
@@ -97,6 +101,12 @@ module IntegrationsHelper
form_data
end
+ def integration_overrides_data(integration)
+ {
+ overrides_path: scoped_overrides_integration_path(integration, format: :json)
+ }
+ end
+
def integration_list_data(integrations)
{
integrations: integrations.map { |i| serialize_integration(i) }.to_json
@@ -115,6 +125,15 @@ module IntegrationsHelper
!Gitlab.com?
end
+ def integration_tabs(integration:)
+ [
+ { key: 'edit', text: _('Settings'), href: scoped_edit_integration_path(integration) },
+ (
+ { key: 'overrides', text: s_('Integrations|Projects using custom settings'), href: scoped_overrides_integration_path(integration) } if integration.instance_level?
+ )
+ ].compact
+ end
+
def jira_issue_breadcrumb_link(issue_reference)
link_to '', { class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do
icon = image_tag image_path('illustrations/logos/jira.svg'), width: 15, height: 15, class: 'gl-mr-2'
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 3c290701a5f..5134b484249 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -4,21 +4,17 @@ module InviteMembersHelper
include Gitlab::Utils::StrongMemoize
def can_invite_members_for_project?(project)
- Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
+ # do not use the can_admin_project_member? helper here due to structure of the view and how membership_locked?
+ # is leveraged for inviting groups
+ Feature.enabled?(:invite_members_group_modal, project.group) && can?(current_user, :admin_project_member, project)
end
def can_invite_group_for_project?(project)
- Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project) && project.allowed_to_share_with_group?
- end
-
- def directly_invite_members?
- strong_memoize(:directly_invite_members) do
- can_import_members?
- end
- end
-
- def invite_group_members?(group)
- experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
+ # do not use the can_admin_project_member? helper here due to structure of the view and how membership_locked?
+ # is leveraged for inviting groups
+ Feature.enabled?(:invite_members_group_modal, project.group) &&
+ can?(current_user, :admin_project_member, project) &&
+ project.allowed_to_share_with_group?
end
def invite_accepted_notice(member)
@@ -39,4 +35,48 @@ module InviteMembersHelper
{}
end
end
+
+ def common_invite_modal_dataset(source)
+ dataset = {
+ id: source.id,
+ name: source.name,
+ default_access_level: Gitlab::Access::GUEST
+ }
+
+ experiment(:member_areas_of_focus, user: current_user) do |e|
+ e.publish_to_database
+
+ e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) }
+ e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) }
+ end
+
+ dataset
+ end
+
+ private
+
+ def member_areas_of_focus_options
+ [
+ {
+ value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase')
+ },
+ {
+ value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests')
+ },
+ {
+ value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD')
+ },
+ {
+ value: 'Configure security features', text: s_('InviteMembersModal|Configure security features')
+ },
+ {
+ value: 'Other', text: s_('InviteMembersModal|Other')
+ }
+ ]
+ end
+
+ # Overridden in EE
+ def users_filter_data(group)
+ {}
+ end
end
diff --git a/app/helpers/issuables_description_templates_helper.rb b/app/helpers/issuables_description_templates_helper.rb
index 6cafde65c5c..a5b9a6eee80 100644
--- a/app/helpers/issuables_description_templates_helper.rb
+++ b/app/helpers/issuables_description_templates_helper.rb
@@ -32,13 +32,14 @@ module IssuablesDescriptionTemplatesHelper
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names(project, issuable_type.pluralize)
end
- def issuable_templates_names(issuable)
+ # Overriden on EE::IssuablesDescriptionTemplatesHelper to include inherited templates names
+ def issuable_templates_names(issuable, include_inherited_templates = false)
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
end
def selected_template(issuable)
- params[:issuable_template] if issuable_templates_names(issuable).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
+ params[:issuable_template] if issuable_templates_names(issuable, true).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
end
def template_names_path(parent, issuable)
@@ -47,3 +48,5 @@ module IssuablesDescriptionTemplatesHelper
project_template_names_path(parent, template_type: issuable.to_ability_name)
end
end
+
+IssuablesDescriptionTemplatesHelper.prepend_mod
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 5bedfc61d46..bbafdac9a7f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -48,10 +48,22 @@ module IssuesHelper
end
end
+ def work_item_type_icon(issue_type)
+ if WorkItem::Type.base_types.include?(issue_type)
+ "issue-type-#{issue_type.to_s.dasherize}"
+ else
+ 'issue-type-issue'
+ end
+ end
+
def confidential_icon(issue)
sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
end
+ def hidden_issue_icon(issue)
+ sprite_icon('spam', css_class: 'gl-vertical-align-text-bottom') if issue.hidden?
+ end
+
def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
@@ -195,7 +207,7 @@ module IssuesHelper
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('integration/jira/', anchor: 'view-jira-issues'),
+ jira_integration_path: help_page_url('integration/jira/issues', 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: { milestone_id: finder.milestones.first.try(:id) }),
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 7525481047e..f185d6cd002 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -250,7 +250,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
- class: 'toolbar-btn js-md has-tooltip',
+ class: 'gl-button btn btn-default-tertiary btn-icon js-md has-tooltip',
data: data,
title: options[:title],
aria: { label: options[:title] } do
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 81059834136..d5d692f2d6e 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -186,7 +186,10 @@ module MergeRequestsHelper
show_suggest_popover: show_suggest_popover?.to_s,
show_whitespace_default: @show_whitespace_default.to_s,
file_by_file_default: @file_by_file_default.to_s,
- default_suggestion_commit_message: default_suggestion_commit_message
+ default_suggestion_commit_message: default_suggestion_commit_message,
+ source_project_default_url: @merge_request.source_project && default_url_to_repo(@merge_request.source_project),
+ source_project_full_path: @merge_request.source_project&.full_path,
+ is_forked: @project.forked?.to_s
}
end
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index ff8839d68fd..0384f82f1f1 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -101,7 +101,7 @@ module Nav
)
end
- if Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_import_members?
+ if Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_admin_project_member?(project)
menu_items.push(
invite_members_menu_item(
href: project_project_members_path(project)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 6c57a31f3db..9a6630ec3cb 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -67,15 +67,6 @@ module NavHelper
%w(dev_ops_report usage_trends)
end
- def group_issues_sub_menu_items
- %w[
- groups#issues
- milestones#index
- boards#index
- boards#show
- ]
- end
-
private
def get_header_links
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index 50984415aa5..1a466c9d170 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -64,9 +64,10 @@ module PackagesHelper
project.container_repositories.exists?
end
- def package_details_data(project, package = nil)
+ def package_details_data(project, package, use_presenter = false)
{
- package: package ? package_from_presenter(package) : nil,
+ package: use_presenter ? package_from_presenter(package) : nil,
+ package_id: package.id,
can_delete: can?(current_user, :destroy_package, project).to_s,
svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm),
diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb
index 0871d5638b8..514737b1417 100644
--- a/app/helpers/projects/project_members_helper.rb
+++ b/app/helpers/projects/project_members_helper.rb
@@ -1,32 +1,6 @@
# frozen_string_literal: true
module Projects::ProjectMembersHelper
- def can_manage_project_members?(project)
- can?(current_user, :admin_project_member, project)
- end
-
- def show_groups?(group_links)
- group_links.exists? || groups_tab_active?
- end
-
- def show_invited_members?(project, invited_members)
- can_manage_project_members?(project) && invited_members.exists?
- end
-
- def show_access_requests?(project, requesters)
- can_manage_project_members?(project) && requesters.exists?
- end
-
- def groups_tab_active?
- params[:search_groups].present?
- end
-
- def current_user_is_group_owner?(project)
- return false if project.group.nil?
-
- project.group.has_owner?(current_user)
- end
-
def project_members_app_data_json(project, members:, group_links:, invited:, access_requests:)
{
user: project_members_list_data(project, members, { param_name: :page, params: { search_groups: nil } }),
@@ -34,7 +8,7 @@ module Projects::ProjectMembersHelper
invite: project_members_list_data(project, invited.nil? ? [] : invited),
access_request: project_members_list_data(project, access_requests.nil? ? [] : access_requests),
source_id: project.id,
- can_manage_members: can_manage_project_members?(project)
+ can_manage_members: Ability.allowed?(current_user, :admin_project_member, project)
}.to_json
end
diff --git a/app/helpers/projects/terraform_helper.rb b/app/helpers/projects/terraform_helper.rb
index 621d97ffb69..fb35224fad3 100644
--- a/app/helpers/projects/terraform_helper.rb
+++ b/app/helpers/projects/terraform_helper.rb
@@ -5,7 +5,10 @@ module Projects::TerraformHelper
{
empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'),
project_path: project.full_path,
- terraform_admin: current_user&.can?(:admin_terraform_state, project)
+ terraform_admin: current_user&.can?(:admin_terraform_state, project),
+ access_tokens_path: profile_personal_access_tokens_path,
+ username: current_user&.username,
+ terraform_api_url: "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state"
}
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 752e91df9c4..f30223f6f1e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -291,8 +291,8 @@ module ProjectsHelper
) % { default_label: default_label }
end
- def can_import_members?
- Ability.allowed?(current_user, :admin_project_member, @project)
+ def can_admin_project_member?(project)
+ Ability.allowed?(current_user, :admin_project_member, project) && !membership_locked?
end
def project_can_be_shared?
@@ -354,6 +354,29 @@ module ProjectsHelper
project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty?
end
+ def project_permissions_panel_data(project)
+ {
+ packagesAvailable: ::Gitlab.config.packages.enabled,
+ packagesHelpPath: help_page_path('user/packages/index'),
+ currentSettings: project_permissions_settings(project),
+ canDisableEmails: can_disable_emails?(project, current_user),
+ canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
+ allowedVisibilityOptions: project_allowed_visibility_levels(project),
+ visibilityHelpPath: help_page_path('public_access/public_access'),
+ registryAvailable: Gitlab.config.registry.enabled,
+ registryHelpPath: help_page_path('user/packages/container_registry/index'),
+ lfsAvailable: Gitlab.config.lfs.enabled,
+ lfsHelpPath: help_page_path('topics/git/lfs/index'),
+ lfsObjectsExist: project.lfs_objects.exists?,
+ lfsObjectsRemovalHelpPath: help_page_path('topics/git/lfs/index', anchor: 'removing-objects-from-lfs'),
+ pagesAvailable: Gitlab.config.pages.enabled,
+ pagesAccessControlEnabled: Gitlab.config.pages.access_control,
+ pagesAccessControlForced: ::Gitlab::Pages.access_control_is_forced?,
+ pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control'),
+ issuesHelpPath: help_page_path('user/project/issues/index')
+ }
+ end
+
private
def tab_ability_map
@@ -491,7 +514,6 @@ module ProjectsHelper
def project_permissions_settings(project)
feature = project.project_feature
-
{
packagesEnabled: !!project.packages_enabled,
visibilityLevel: project.visibility_level,
@@ -511,38 +533,11 @@ module ProjectsHelper
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
operationsAccessLevel: feature.operations_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis?,
- allowEditingCommitMessages: project.allow_editing_commit_messages?,
- securityAndComplianceAccessLevel: project.security_and_compliance_access_level
+ securityAndComplianceAccessLevel: project.security_and_compliance_access_level,
+ containerRegistryAccessLevel: feature.container_registry_access_level
}
end
- def project_permissions_panel_data(project)
- {
- packagesAvailable: ::Gitlab.config.packages.enabled,
- packagesHelpPath: help_page_path('user/packages/index'),
- currentSettings: project_permissions_settings(project),
- canDisableEmails: can_disable_emails?(project, current_user),
- canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
- allowedVisibilityOptions: project_allowed_visibility_levels(project),
- visibilityHelpPath: help_page_path('public_access/public_access'),
- registryAvailable: Gitlab.config.registry.enabled,
- registryHelpPath: help_page_path('user/packages/container_registry/index'),
- lfsAvailable: Gitlab.config.lfs.enabled,
- lfsHelpPath: help_page_path('topics/git/lfs/index'),
- lfsObjectsExist: project.lfs_objects.exists?,
- lfsObjectsRemovalHelpPath: help_page_path('topics/git/lfs/index', anchor: 'removing-objects-from-lfs'),
- pagesAvailable: Gitlab.config.pages.enabled,
- pagesAccessControlEnabled: Gitlab.config.pages.access_control,
- pagesAccessControlForced: ::Gitlab::Pages.access_control_is_forced?,
- pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control'),
- issuesHelpPath: help_page_path('user/project/issues/index')
- }
- end
-
- def project_permissions_panel_data_json(project)
- project_permissions_panel_data(project).to_json.html_safe
- end
-
def project_allowed_visibility_levels(project)
Gitlab::VisibilityLevel.values.select do |level|
project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index ec8ed3d6e7f..409a3e65fe3 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -150,7 +150,7 @@ module SearchHelper
}
]
- if search_service.scope == 'issues' && Feature.enabled?(:search_sort_issues_by_popularity)
+ if search_service.scope == 'issues'
options << {
title: _('Popularity'),
sortable: true,
@@ -189,7 +189,7 @@ module SearchHelper
{ category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
{ category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
{ category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
- { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
+ { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/index") },
{ category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/index") },
{ category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") }
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 84eb0405c01..e9ef9146529 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -39,6 +39,12 @@ module SnippetsHelper
end
end
+ def snippet_report_abuse_path(snippet)
+ return unless snippet.submittable_as_spam_by?(current_user)
+
+ mark_as_spam_snippet_path(snippet)
+ end
+
def embedded_raw_snippet_button(snippet, blob)
return if blob.empty? || blob.binary? || blob.stored_externally?
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index da32dfb0b9b..7fa85d143f7 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -3,6 +3,7 @@
module SortingHelper
include SortingTitlesValuesHelper
+ # rubocop: disable Metrics/AbcSize
def sort_options_hash
{
sort_value_created_date => sort_title_created_date,
@@ -29,6 +30,9 @@ module SortingHelper
sort_value_merged_date => sort_title_merged_date,
sort_value_merged_recently => sort_title_merged_recently,
sort_value_merged_earlier => sort_title_merged_earlier,
+ sort_value_closed_date => sort_title_closed_date,
+ sort_value_closed_recently => sort_title_closed_recently,
+ sort_value_closed_earlier => sort_title_closed_earlier,
sort_value_upvotes => sort_title_upvotes,
sort_value_contacted_date => sort_title_contacted_date,
sort_value_relative_position => sort_title_relative_position,
@@ -36,6 +40,7 @@ module SortingHelper
sort_value_expire_date => sort_title_expire_date
}
end
+ # rubocop: enable Metrics/AbcSize
def projects_sort_options_hash
use_old_sorting = Feature.disabled?(:project_list_filter_bar) || current_controller?('admin/projects')
@@ -182,6 +187,7 @@ module SortingHelper
sort_value_milestone_later => sort_value_milestone,
sort_value_due_date_later => sort_value_due_date,
sort_value_merged_recently => sort_value_merged_date,
+ sort_value_closed_recently => sort_value_closed_date,
sort_value_least_popular => sort_value_popularity
}
end
@@ -196,6 +202,8 @@ module SortingHelper
sort_value_due_date_soon => sort_value_due_date_later,
sort_value_merged_date => sort_value_merged_recently,
sort_value_merged_earlier => sort_value_merged_recently,
+ sort_value_closed_date => sort_value_closed_recently,
+ sort_value_closed_earlier => sort_value_closed_recently,
sort_value_popularity => sort_value_least_popular,
sort_value_most_popular => sort_value_least_popular
}.merge(issuable_sort_option_overrides)
@@ -216,7 +224,7 @@ module SortingHelper
def sort_direction_icon(sort_value)
case sort_value
- when sort_value_milestone, sort_value_due_date, sort_value_merged_date, /_asc\z/
+ when sort_value_milestone, sort_value_due_date, sort_value_merged_date, sort_value_closed_date, /_asc\z/
'sort-lowest'
else
'sort-highest'
diff --git a/app/helpers/sorting_titles_values_helper.rb b/app/helpers/sorting_titles_values_helper.rb
index 9b839f4e9bc..f4117d690f3 100644
--- a/app/helpers/sorting_titles_values_helper.rb
+++ b/app/helpers/sorting_titles_values_helper.rb
@@ -38,6 +38,18 @@ module SortingTitlesValuesHelper
s_('SortOptions|Merged earlier')
end
+ def sort_title_closed_date
+ s_('SortOptions|Closed date')
+ end
+
+ def sort_title_closed_recently
+ s_('SortOptions|Closed recently')
+ end
+
+ def sort_title_closed_earlier
+ s_('SortOptions|Closed earlier')
+ end
+
def sort_title_largest_group
s_('SortOptions|Largest group')
end
@@ -199,6 +211,18 @@ module SortingTitlesValuesHelper
'merged_at_asc'
end
+ def sort_value_closed_date
+ 'closed_at'
+ end
+
+ def sort_value_closed_recently
+ 'closed_at_desc'
+ end
+
+ def sort_value_closed_earlier
+ 'closed_at_asc'
+ end
+
def sort_value_largest_group
'storage_size_desc'
end
diff --git a/app/helpers/time_zone_helper.rb b/app/helpers/time_zone_helper.rb
index fe045182c96..f92e32ff9b6 100644
--- a/app/helpers/time_zone_helper.rb
+++ b/app/helpers/time_zone_helper.rb
@@ -31,4 +31,9 @@ module TimeZoneHelper
}.slice(*attrs)
end
end
+
+ def local_time(timezone)
+ time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone
+ time_zone_instance.now.strftime("%-l:%M %p")
+ end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 54c03d3d966..d1f33f99ad0 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -178,7 +178,7 @@ module TreeHelper
def web_ide_button_data(options = {})
{
project_path: project_to_use.full_path,
- ref: ActionDispatch::Journey::Router::Utils.escape_path(@ref),
+ ref: @ref,
is_fork: fork?,
needs_to_fork: needs_to_fork?,
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 4e6af298fcd..f5a74a3f57d 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -4,7 +4,6 @@ module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'
GCP_SIGNUP_OFFER = 'gcp_signup_offer'
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
- SERVICE_TEMPLATES_DEPRECATED_CALLOUT = 'service_templates_deprecated_callout'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
@@ -35,13 +34,6 @@ module UserCalloutsHelper
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
- def show_service_templates_deprecated_callout?
- !Gitlab.com? &&
- current_user&.admin? &&
- Integration.for_template.active.exists? &&
- !user_dismissed?(SERVICE_TEMPLATES_DEPRECATED_CALLOUT)
- end
-
def show_customize_homepage_banner?
current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end
@@ -61,6 +53,9 @@ module UserCalloutsHelper
!user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
end
+ def dismiss_account_recovery_regular_check
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
@@ -70,4 +65,4 @@ module UserCalloutsHelper
end
end
-UserCalloutsHelper.prepend_mod_with('UserCalloutsHelper')
+UserCalloutsHelper.prepend_mod
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 93a0166f43e..c64c2ab35fb 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -184,7 +184,7 @@ module UsersHelper
activate: activate_admin_user_path(:id),
unlock: unlock_admin_user_path(:id),
delete: admin_user_path(:id),
- delete_with_contributions: admin_user_path(:id),
+ delete_with_contributions: admin_user_path(:id, hard_delete: true),
admin_user: admin_user_path(:id),
ban: ban_admin_user_path(:id),
unban: unban_admin_user_path(:id)
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index d1870065845..fe2d2891547 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -6,6 +6,8 @@ module Emails
include MembersHelper
include Gitlab::Experiment::Dsl
+ INITIAL_INVITE = 'initial_email'
+
included do
helper_method :member_source, :member
helper_method :experiment
@@ -53,6 +55,8 @@ module Emails
return unless member_exists?
+ Gitlab::Tracking.event(self.class.name, 'invite_email_sent', label: 'invite_email', property: member_id.to_s)
+
mail(to: member.invite_email, subject: invite_email_subject, **invite_email_headers) do |format|
format.html { render layout: 'unknown_user_mailer' }
format.text { render layout: 'unknown_user_mailer' }
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index fb1f70723fd..1b27d062391 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -32,7 +32,7 @@ module Emails
# thousand times. This could be potentially expensive in a loop, and
# recipients would contain all project watchers so it could be a lot.
mail(bcc: recipients,
- subject: pipeline_subject(status)) do |format|
+ subject: subject(pipeline_subject(status))) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
@@ -53,7 +53,6 @@ module Emails
subject = []
subject << "#{status} pipeline for #{@pipeline.source_ref}"
- subject << @project.name if @project
subject << @pipeline.short_sha
subject.join(' | ')
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 2ae82b49609..06ba16f9724 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -60,9 +60,32 @@ module Emails
@project = project
@alert = alert.present
+ add_project_headers
+ add_alert_headers
+
subject_text = "Alert: #{@alert.email_title}"
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
end
+
+ private
+
+ def add_alert_headers
+ return unless @alert
+
+ headers['X-GitLab-Alert-ID'] = @alert.id
+ headers['X-GitLab-Alert-IID'] = @alert.iid
+ headers['X-GitLab-NotificationReason'] = "alert_#{@alert.state}"
+
+ add_incident_headers
+ end
+
+ def add_incident_headers
+ incident = @alert.issue
+ return unless incident
+
+ headers['X-GitLab-Incident-ID'] = incident.id
+ headers['X-GitLab-Incident-IID'] = incident.iid
+ end
end
end
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index ceeb178e9c2..13b24e099c9 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -246,7 +246,7 @@ class NotifyPreview < ActionMailer::Preview
def cleanup
email = nil
- ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases
email = yield
raise ActiveRecord::Rollback
end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index d0e4163dcdb..f40d0cd2fa4 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -9,24 +9,12 @@ module AlertManagement
include ShaAttribute
include Sortable
include Noteable
+ include Mentionable
include Gitlab::SQL::Pattern
include Presentable
include Gitlab::Utils::StrongMemoize
include Referable
-
- STATUSES = {
- triggered: 0,
- acknowledged: 1,
- resolved: 2,
- ignored: 3
- }.freeze
-
- 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
+ include ::IncidentManagement::Escalatable
belongs_to :project
belongs_to :issue, optional: true
@@ -44,6 +32,9 @@ module AlertManagement
sha_attribute :fingerprint
+ # Allow :ended_at to be managed by Escalatable
+ alias_attribute :resolved_at, :ended_at
+
TITLE_MAX_LENGTH = 200
DESCRIPTION_MAX_LENGTH = 1_000
SERVICE_MAX_LENGTH = 100
@@ -57,7 +48,6 @@ module AlertManagement
validates :project, presence: true
validates :events, presence: true
validates :severity, presence: true
- validates :status, presence: true
validates :started_at, presence: true
validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project,
@@ -80,52 +70,10 @@ module AlertManagement
threat_monitoring: 1
}
- state_machine :status, initial: :triggered do
- state :triggered, value: STATUSES[:triggered]
-
- state :acknowledged, value: STATUSES[:acknowledged]
-
- state :resolved, value: STATUSES[:resolved] do
- validates :ended_at, presence: true
- end
-
- state :ignored, value: STATUSES[:ignored]
-
- state :triggered, :acknowledged, :ignored do
- validates :ended_at, absence: true
- end
-
- event :trigger do
- transition any => :triggered
- end
-
- event :acknowledge do
- transition any => :acknowledged
- end
-
- event :resolve do
- transition any => :resolved
- end
-
- event :ignore do
- transition any => :ignored
- end
-
- before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
- alert.ended_at = nil
- end
-
- before_transition to: :resolved do |alert, transition|
- ended_at = transition.args.first
- alert.ended_at = ended_at || Time.current
- end
- end
-
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
- scope :for_status, -> (status) { with_status(status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
scope :for_environment, -> (environment) { where(environment: environment) }
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
@@ -146,36 +94,14 @@ module AlertManagement
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
- # Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
- # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
- # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
- scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
-
scope :counts_by_project_id, -> { group(:project_id).count }
alias_method :state, :status_name
- def self.state_machine_statuses
- @state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
- end
- private_class_method :state_machine_statuses
-
- def self.status_value(name)
- state_machine_statuses[name]
- end
-
- def self.status_name(raw_status)
- state_machine_statuses.key(raw_status)
- end
-
def self.counts_by_status
group(:status).count.transform_keys { |k| status_name(k) }
end
- def self.status_names
- @status_names ||= state_machine_statuses.keys
- end
-
def self.sort_by_attribute(method)
case method.to_s
when 'started_at_asc' then order_start_time(:asc)
@@ -229,15 +155,6 @@ module AlertManagement
self.class.open_status?(status_name)
end
- def status_event_for(status)
- self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
- end
-
- def change_status_to(new_status)
- event = status_event_for(new_status)
- event && fire_status_event(event)
- end
-
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
diff --git a/app/models/analytics/cycle_analytics/stage_event_hash.rb b/app/models/analytics/cycle_analytics/stage_event_hash.rb
new file mode 100644
index 00000000000..0e1e9b3ef67
--- /dev/null
+++ b/app/models/analytics/cycle_analytics/stage_event_hash.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ class StageEventHash < ApplicationRecord
+ has_many :cycle_analytics_project_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :stage_event_hash
+
+ validates :hash_sha256, presence: true
+
+ # Creates or queries the id of the corresponding stage event hash code
+ def self.record_id_by_hash_sha256(hash)
+ casted_hash_code = Arel::Nodes.build_quoted(hash, Analytics::CycleAnalytics::StageEventHash.arel_table[:hash_sha256]).to_sql
+
+ # Atomic, safe insert without retrying
+ query = <<~SQL
+ WITH insert_cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
+ INSERT INTO #{quoted_table_name} (hash_sha256) VALUES (#{casted_hash_code}) ON CONFLICT DO NOTHING RETURNING ID
+ )
+ SELECT ids.id FROM (
+ (SELECT id FROM #{quoted_table_name} WHERE hash_sha256=#{casted_hash_code} LIMIT 1)
+ UNION ALL
+ (SELECT id FROM insert_cte LIMIT 1)
+ ) AS ids LIMIT 1
+ SQL
+
+ connection.execute(query).first['id']
+ end
+
+ def self.cleanup_if_unused(id)
+ unused_hashes_for(id)
+ .where(id: id)
+ .delete_all
+ end
+
+ def self.unused_hashes_for(id)
+ exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1)
+ where.not('EXISTS (?)', exists_query)
+ end
+ end
+ end
+end
+Analytics::CycleAnalytics::StageEventHash.prepend_mod_with('Analytics::CycleAnalytics::StageEventHash')
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 527b67712ee..d9375b55e89 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -63,11 +63,27 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args, &block)
+ return optimized_safe_find_or_create_by(*args, &block) if Feature.enabled?(:optimize_safe_find_or_create_by, default_enabled: :yaml)
+
safe_ensure_unique(retries: 1) do
find_or_create_by(*args, &block)
end
end
+ def self.optimized_safe_find_or_create_by(*args, &block)
+ record = find_by(*args)
+ return record if record.present?
+
+ # We need to use `all.create` to make this implementation follow `find_or_create_by` which delegates this in
+ # https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/querying.rb#L22
+ #
+ # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
+ # and that skips some code that adds the newly created record to the association.
+ transaction(requires_new: true) { all.create(*args, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by(*args)
+ end
+
def create_or_load_association(association_name)
association(association_name).create unless association(association_name).loaded?
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
@@ -87,6 +103,23 @@ class ApplicationRecord < ActiveRecord::Base
enum(enum_mod.key => values)
end
+ def self.transaction(**options, &block)
+ if options[:requires_new] && track_subtransactions?
+ ::Gitlab::Database::Metrics.subtransactions_increment(self.name)
+ end
+
+ super(**options, &block)
+ end
+
+ def self.track_subtransactions?
+ ::Feature.enabled?(:active_record_subtransactions_counter, type: :ops, default_enabled: :yaml) &&
+ connection.transaction_open?
+ end
+
+ def self.cached_column_list
+ self.column_names.map { |column_name| self.arel_table[column_name] }
+ end
+
def readable_by?(user)
Ability.allowed?(user, "read_#{to_ability_name}".to_sym, self)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a7140cc0718..c4b6bcb9395 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,6 +5,11 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
+ include IgnorableColumns
+
+ ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
+ ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22'
+ ignore_column :cloud_license_enabled, remove_with: '14.4', remove_after: '2021-09-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -30,7 +35,7 @@ class ApplicationSetting < ApplicationRecord
def self.kroki_formats_attributes
{
blockdiag: {
- label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag and RackDiag)'
+ label: 'BlockDiag (includes BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, and RackDiag)'
},
bpmn: {
label: 'BPMN'
@@ -451,6 +456,9 @@ class ApplicationSetting < ApplicationRecord
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
+ validates :customers_dot_jwt_signing_key,
+ rsa_key: true, allow_nil: true
+
validates :rate_limiting_response_text,
length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
allow_blank: true
@@ -554,6 +562,7 @@ class ApplicationSetting < ApplicationRecord
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 :customers_dot_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
@@ -564,6 +573,7 @@ class ApplicationSetting < ApplicationRecord
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
+ before_validation :sanitize_default_branch_name
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -593,6 +603,14 @@ class ApplicationSetting < ApplicationRecord
!!(sourcegraph_url =~ %r{\Ahttps://(www\.)?sourcegraph\.com})
end
+ def sanitize_default_branch_name
+ self.default_branch_name = if default_branch_name.blank?
+ nil
+ else
+ Sanitize.fragment(self.default_branch_name)
+ end
+ end
+
def instance_review_permitted?
users_count = Rails.cache.fetch('limited_users_count', expires_in: 1.day) do
::User.limit(INSTANCE_REVIEW_MIN_USERS + 1).count(:all)
@@ -627,7 +645,7 @@ class ApplicationSetting < ApplicationRecord
# prevent this from happening, we do a sanity check that the
# primary key constraint is present before inserting a new entry.
def self.check_schema!
- return if ActiveRecord::Base.connection.primary_key(self.table_name).present?
+ return if connection.primary_key(self.table_name).present?
raise "The `#{self.table_name}` table is missing a primary key constraint in the database schema"
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index d7a594af84c..060c831a11b 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -219,11 +219,11 @@ module ApplicationSettingImplementation
end
def home_page_url_column_exists?
- ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url)
+ ::Gitlab::Database.main.cached_column_exists?(:application_settings, :home_page_url)
end
def help_page_support_url_column_exists?
- ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url)
+ ::Gitlab::Database.main.cached_column_exists?(:application_settings, :help_page_support_url)
end
def disabled_oauth_sign_in_sources=(sources)
diff --git a/app/models/ci/application_record.rb b/app/models/ci/application_record.rb
new file mode 100644
index 00000000000..9d4a8f0648e
--- /dev/null
+++ b/app/models/ci/application_record.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Ci
+ class ApplicationRecord < ::ApplicationRecord
+ self.abstract_class = true
+
+ def self.table_name_prefix
+ 'ci_'
+ end
+
+ def self.model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.demodulize)
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4328f3f7a4b..1ca291a659b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -39,7 +39,6 @@ module Ci
has_one :pending_state, class_name: 'Ci::BuildPendingState', inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id
- has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
@@ -54,6 +53,7 @@ module Ci
end
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
+ has_one :trace_metadata, class_name: 'Ci::BuildTraceMetadata', inverse_of: :build
accepts_nested_attributes_for :runner_session, update_only: true
accepts_nested_attributes_for :job_variables
@@ -103,7 +103,6 @@ module Ci
end
scope :unstarted, -> { where(runner_id: nil) }
- scope :ignore_failures, -> { where(allow_failure: false) }
scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
@@ -120,10 +119,6 @@ module Ci
where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
end
- scope :with_archived_trace, -> do
- with_existing_job_artifacts(Ci::JobArtifact.trace)
- end
-
scope :without_archived_trace, -> do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
@@ -134,7 +129,6 @@ module Ci
end
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
- scope :eager_load_job_artifacts_archive, -> { includes(:job_artifacts_archive) }
scope :eager_load_tags, -> { includes(:tags) }
scope :eager_load_everything, -> do
@@ -158,7 +152,7 @@ module Ci
scope :with_project_and_metadata, -> do
if Feature.enabled?(:non_public_artifacts, type: :development)
- joins(:metadata).includes(:project, :metadata)
+ joins(:metadata).includes(:metadata).preload(:project)
end
end
@@ -466,13 +460,9 @@ module Ci
end
def retryable?
- if Feature.enabled?(:prevent_retry_of_retried_jobs, project, default_enabled: :yaml)
- return false if retried? || archived?
+ return false if retried? || archived?
- success? || failed? || canceled?
- else
- !archived? && (success? || failed? || canceled?)
- end
+ success? || failed? || canceled?
end
def retries_count
@@ -559,6 +549,7 @@ module Ci
.concat(persisted_variables)
.concat(dependency_proxy_variables)
.concat(job_jwt_variables)
+ .concat(kubernetes_variables)
.concat(scoped_variables)
.concat(job_variables)
.concat(persisted_environment_variables)
@@ -648,12 +639,6 @@ module Ci
update(coverage: coverage) if coverage.present?
end
- # rubocop: disable CodeReuse/ServiceClass
- def parse_trace_sections!
- ExtractSectionsFromBuildTraceService.new(project, user).execute(self)
- end
- # rubocop: enable CodeReuse/ServiceClass
-
def trace
Gitlab::Ci::Trace.new(self)
end
@@ -907,7 +892,7 @@ module Ci
end
def valid_dependency?
- return false if artifacts_expired?
+ return false if artifacts_expired? && !pipeline.artifacts_locked?
return false if erased?
true
@@ -1183,6 +1168,10 @@ module Ci
end
end
+ def kubernetes_variables
+ [] # Overridden in EE
+ end
+
def conditionally_allow_failure!(exit_code)
return unless exit_code
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 50775f578f0..90237a4be52 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -3,10 +3,9 @@
module Ci
# The purpose of this class is to store Build related data that can be disposed.
# Data that should be persisted forever, should be stored with Ci::Build model.
- class BuildMetadata < ApplicationRecord
+ class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
- extend Gitlab::Ci::Model
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 4a59c25cbb0..003659570b3 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildNeed < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class BuildNeed < Ci::ApplicationRecord
include BulkInsertSafe
include IgnorableColumns
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 299c67f441d..53cf0697e2e 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-class Ci::BuildPendingState < ApplicationRecord
- extend Gitlab::Ci::Model
-
+class Ci::BuildPendingState < Ci::ApplicationRecord
belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
enum state: Ci::Stage.statuses
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index eb6a0700006..2c08fc4c8bf 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class BuildReportResult < Ci::ApplicationRecord
self.primary_key = :build_id
belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 2aa856dbc64..45de47116cd 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -3,8 +3,7 @@
module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
- class BuildRunnerSession < ApplicationRecord
- extend Gitlab::Ci::Model
+ class BuildRunnerSession < Ci::ApplicationRecord
include IgnorableColumns
ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 3fa9a484b0c..7a15d7ba940 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class BuildTraceChunk < ApplicationRecord
- extend ::Gitlab::Ci::Model
+ class BuildTraceChunk < Ci::ApplicationRecord
include ::Comparable
include ::FastDestroyAll
include ::Checksummable
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
new file mode 100644
index 00000000000..05bdb3d8b7b
--- /dev/null
+++ b/app/models/ci/build_trace_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildTraceMetadata < Ci::ApplicationRecord
+ self.table_name = 'ci_build_trace_metadata'
+ self.primary_key = :build_id
+
+ belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :trace_artifact, class_name: 'Ci::JobArtifact'
+
+ validates :build, presence: true
+ end
+end
diff --git a/app/models/ci/build_trace_section.rb b/app/models/ci/build_trace_section.rb
deleted file mode 100644
index 036f611a61c..00000000000
--- a/app/models/ci/build_trace_section.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class BuildTraceSection < ApplicationRecord
- extend SuppressCompositePrimaryKeyWarning
- extend Gitlab::Ci::Model
- include IgnorableColumns
-
- belongs_to :build, class_name: 'Ci::Build'
- belongs_to :project
- belongs_to :section_name, class_name: 'Ci::BuildTraceSectionName'
-
- validates :section_name, :build, :project, presence: true, allow_blank: false
-
- ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
- end
-end
diff --git a/app/models/ci/build_trace_section_name.rb b/app/models/ci/build_trace_section_name.rb
deleted file mode 100644
index c065cfea14e..00000000000
--- a/app/models/ci/build_trace_section_name.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class BuildTraceSectionName < ApplicationRecord
- extend Gitlab::Ci::Model
-
- belongs_to :project
- has_many :trace_sections, class_name: 'Ci::BuildTraceSection', foreign_key: :section_name_id
-
- validates :name, :project, presence: true, allow_blank: false
- validates :name, uniqueness: { scope: :project_id }
- end
-end
diff --git a/app/models/ci/base_model.rb b/app/models/ci/ci_database_record.rb
index 8fb752ead1d..e2b832a28e7 100644
--- a/app/models/ci/base_model.rb
+++ b/app/models/ci/ci_database_record.rb
@@ -7,7 +7,7 @@ module Ci
# This class is part of a migration to move all CI classes to a new separate database.
# Initially we are only going to be moving the `Ci::InstanceVariable` model and it will be duplicated in the main and CI tables
# Do not extend this class in any other models.
- class BaseModel < ::ApplicationRecord
+ class CiDatabaseRecord < Ci::ApplicationRecord
self.abstract_class = true
if Gitlab::Database.has_config?(:ci)
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index b46d32474c6..598d1456a48 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class DailyBuildGroupReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class DailyBuildGroupReportResult < Ci::ApplicationRecord
PARAM_TYPES = %w[coverage].freeze
belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
diff --git a/app/models/ci/deleted_object.rb b/app/models/ci/deleted_object.rb
index b2a949c9bb5..aba7b73aba9 100644
--- a/app/models/ci/deleted_object.rb
+++ b/app/models/ci/deleted_object.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class DeletedObject < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class DeletedObject < Ci::ApplicationRecord
mount_uploader :file, DeletedObjectUploader
scope :ready_for_destruction, ->(limit) do
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index d215372bb45..da0bbbacddd 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
module Ci
- class FreezePeriod < ApplicationRecord
+ class FreezePeriod < Ci::ApplicationRecord
include StripAttribute
- self.table_name = 'ci_freeze_periods'
+ include Ci::NamespacedModelName
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
belongs_to :project, inverse_of: :freeze_periods
- strip_attributes :freeze_start, :freeze_end
+ strip_attributes! :freeze_start, :freeze_end
validates :freeze_start, cron: true, presence: true
validates :freeze_end, cron: true, presence: true
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index 2928ce801ad..165bee5c54d 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class GroupVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class GroupVariable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 5aee4c924af..f4aa935b983 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class InstanceVariable < ::Ci::BaseModel
- extend Gitlab::Ci::Model
+ class InstanceVariable < Ci::CiDatabaseRecord
extend Gitlab::ProcessMemoryCache::Helper
include Ci::NewHasVariable
include Ci::Maskable
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 46c976d5616..1f0da4345f2 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Ci
- class JobArtifact < ApplicationRecord
+ class JobArtifact < Ci::ApplicationRecord
include AfterCommitQueue
include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
@@ -10,7 +10,6 @@ module Ci
include Artifactable
include FileStoreMounter
include EachBatch
- extend Gitlab::Ci::Model
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 7eea8a37150..44bd3fe8901 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class JobVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class JobVariable < Ci::ApplicationRecord
include Ci::NewHasVariable
include BulkInsertSafe
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 0663052f51d..7cf3a387516 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -1,14 +1,16 @@
# frozen_string_literal: true
module Ci
- class PendingBuild < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PendingBuild < Ci::ApplicationRecord
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
+ belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace'
+
+ validates :namespace, presence: true
scope :ref_protected, -> { where(protected: true) }
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
+ scope :with_instance_runners, -> { where(instance_runners_enabled: true) }
def self.upsert_from_build!(build)
entry = self.new(args_from_build(build))
@@ -22,7 +24,8 @@ module Ci
args = {
build: build,
project: build.project,
- protected: build.protected?
+ protected: build.protected?,
+ namespace: build.project.namespace
}
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
@@ -56,3 +59,5 @@ module Ci
private_class_method :builds_access_level?
end
end
+
+Ci::PendingBuild.prepend_mod_with('Ci::PendingBuild')
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5d079f57267..70e67953e31 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Pipeline < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Pipeline < Ci::ApplicationRecord
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -319,6 +318,7 @@ module Ci
scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
scope :eager_load_project, -> { eager_load(project: [:route, { namespace: :route }]) }
+ scope :with_pipeline_source, -> (source) { where(source: source)}
scope :outside_pipeline_family, ->(pipeline) do
where.not(id: pipeline.same_family_pipeline_ids)
@@ -378,11 +378,15 @@ module Ci
end
def self.latest_successful_for_refs(refs)
- relation = newest_first(ref: refs).success
+ return Ci::Pipeline.none if refs.empty?
- relation.each_with_object({}) do |pipeline, hash|
- hash[pipeline.ref] ||= pipeline
- end
+ refs_values = refs.map { |ref| "(#{connection.quote(ref)})" }.join(",")
+ join_query = success.where("refs_values.ref = ci_pipelines.ref").order(id: :desc).limit(1)
+
+ Ci::Pipeline
+ .from("(VALUES #{refs_values}) refs_values (ref)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{Ci::Pipeline.table_name} ON TRUE")
+ .index_by(&:ref)
end
def self.latest_running_for_ref(ref)
@@ -393,6 +397,10 @@ module Ci
newest_first(ref: ref).failed.take
end
+ def self.jobs_count_in_alive_pipelines
+ created_after(24.hours.ago).alive.joins(:builds).count
+ end
+
# Returns a Hash containing the latest pipeline for every given
# commit.
#
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
index 889c5d094a7..2284a05bcc9 100644
--- a/app/models/ci/pipeline_artifact.rb
+++ b/app/models/ci/pipeline_artifact.rb
@@ -3,8 +3,7 @@
# This class is being used to persist additional artifacts after a pipeline completes, which is a great place to cache a computed result in object storage
module Ci
- class PipelineArtifact < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineArtifact < Ci::ApplicationRecord
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
diff --git a/app/models/ci/pipeline_chat_data.rb b/app/models/ci/pipeline_chat_data.rb
index 65466a8c6f8..ba20c993e36 100644
--- a/app/models/ci/pipeline_chat_data.rb
+++ b/app/models/ci/pipeline_chat_data.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Ci
- class PipelineChatData < ApplicationRecord
+ class PipelineChatData < Ci::ApplicationRecord
+ include Ci::NamespacedModelName
+
self.table_name = 'ci_pipeline_chat_data'
belongs_to :chat_name
diff --git a/app/models/ci/pipeline_config.rb b/app/models/ci/pipeline_config.rb
index d5a8da2bc1e..e2dcad653d7 100644
--- a/app/models/ci/pipeline_config.rb
+++ b/app/models/ci/pipeline_config.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineConfig < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PipelineConfig < Ci::ApplicationRecord
self.table_name = 'ci_pipelines_config'
self.primary_key = :pipeline_id
diff --git a/app/models/ci/pipeline_message.rb b/app/models/ci/pipeline_message.rb
index a47ec554462..5668da915e6 100644
--- a/app/models/ci/pipeline_message.rb
+++ b/app/models/ci/pipeline_message.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineMessage < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class PipelineMessage < Ci::ApplicationRecord
MAX_CONTENT_LENGTH = 10_000
belongs_to :pipeline
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index effe2d95a99..b915495ac38 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineSchedule < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineSchedule < Ci::ApplicationRecord
extend ::Gitlab::Utils::Override
include Importable
include StripAttribute
@@ -25,7 +24,7 @@ module Ci
validates :description, presence: true
validates :variables, nested_attributes_duplicates: true
- strip_attributes :cron
+ strip_attributes! :cron
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index adef9911ae1..84a24609cc7 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineScheduleVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineScheduleVariable < Ci::ApplicationRecord
include Ci::HasVariable
belongs_to :pipeline_schedule
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index 84ca4833cd7..a0e8886414b 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class PipelineVariable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class PipelineVariable < Ci::ApplicationRecord
include Ci::HasVariable
belongs_to :pipeline
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index e2f257eab25..30d335fd7d5 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -169,11 +169,7 @@ module Ci
end
def all_dependencies
- if Feature.enabled?(:preload_associations_jobs_request_api_endpoint, project, default_enabled: :yaml)
- strong_memoize(:all_dependencies) do
- dependencies.all
- end
- else
+ strong_memoize(:all_dependencies) do
dependencies.all
end
end
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 3d71a5f2c96..af5fdabff6e 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Ref < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Ref < Ci::ApplicationRecord
include AfterCommitQueue
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/resource.rb b/app/models/ci/resource.rb
index e0e1fab642d..ee094fa2007 100644
--- a/app/models/ci/resource.rb
+++ b/app/models/ci/resource.rb
@@ -1,13 +1,26 @@
# frozen_string_literal: true
module Ci
- class Resource < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class Resource < Ci::ApplicationRecord
belongs_to :resource_group, class_name: 'Ci::ResourceGroup', inverse_of: :resources
belongs_to :processable, class_name: 'Ci::Processable', foreign_key: 'build_id', inverse_of: :resource
scope :free, -> { where(processable: nil) }
+ scope :retained, -> { where.not(processable: nil) }
scope :retained_by, -> (processable) { where(processable: processable) }
+
+ class << self
+ # In some cases, state machine hooks in `Ci::Build` are skipped
+ # even if the job status transitions to a complete state.
+ # For example, `Ci::Build#doom!` (a.k.a `data_integrity_failure`) doesn't execute state machine hooks.
+ # To handle these edge cases, we check the staleness of the jobs that currently
+ # assigned to the resources, and release if it's stale.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/335537#note_632925914 for more information.
+ def stale_processables
+ Ci::Processable.where(id: retained.select(:build_id))
+ .complete
+ .updated_at_before(5.minutes.ago)
+ end
+ end
end
end
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index 85fbe03e1c9..8a7456041e6 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class ResourceGroup < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class ResourceGroup < Ci::ApplicationRecord
belongs_to :project, inverse_of: :resource_groups
has_many :resources, class_name: 'Ci::Resource', inverse_of: :resource_group
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index a541dca47de..432c3a408a9 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Runner < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Runner < Ci::ApplicationRecord
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
@@ -12,6 +11,7 @@ module Ci
include FeatureGate
include Gitlab::Utils::StrongMemoize
include TaggableQueries
+ include Presentable
add_authentication_token_field :token, encrypted: :optional
@@ -61,13 +61,7 @@ module Ci
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) }
scope :recent, -> { where('ci_runners.created_at > :date OR ci_runners.contacted_at > :date', date: 3.months.ago) }
- # The following query using negation is cheaper than using `contacted_at <= ?`
- # because there are less runners online than have been created. The
- # resulting query is quickly finding online ones and then uses the regular
- # indexed search and rejects the ones that are in the previous set. If we
- # did `contacted_at <= ?` the query would effectively have to do a seq
- # scan.
- scope :offline, -> { where.not(id: online) }
+ scope :offline, -> { where(arel_table[:contacted_at].lteq(online_contact_time_deadline)) }
scope :not_connected, -> { where(contacted_at: nil) }
scope :ordered, -> { order(id: :desc) }
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 41a4c9012ff..d1353b97ed9 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
module Ci
- class RunnerNamespace < ApplicationRecord
- extend Gitlab::Ci::Model
+ class RunnerNamespace < Ci::ApplicationRecord
include Limitable
self.limit_name = 'ci_registered_group_runners'
self.limit_scope = :group
self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
+ self.limit_feature_flag_for_override = :ci_runner_limits_override
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 af2595ce4af..e1c435e9b1f 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
module Ci
- class RunnerProject < ApplicationRecord
- extend Gitlab::Ci::Model
+ class RunnerProject < Ci::ApplicationRecord
include Limitable
self.limit_name = 'ci_registered_project_runners'
self.limit_scope = :project
self.limit_relation = :recent_runners
self.limit_feature_flag = :ci_runner_limits
+ self.limit_feature_flag_for_override = :ci_runner_limits_override
belongs_to :runner, inverse_of: :runner_projects
belongs_to :project, inverse_of: :runner_projects
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
index 9446cfa05da..ae38d54862d 100644
--- a/app/models/ci/running_build.rb
+++ b/app/models/ci/running_build.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class RunningBuild < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class RunningBuild < Ci::ApplicationRecord
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :runner, class_name: 'Ci::Runner'
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index f19aac213be..f78caf710a6 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -2,7 +2,9 @@
module Ci
module Sources
- class Pipeline < ApplicationRecord
+ class Pipeline < Ci::ApplicationRecord
+ include Ci::NamespacedModelName
+
self.table_name = "ci_sources_pipelines"
belongs_to :project, class_name: "Project"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d00066b778d..39e26bf2785 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Stage < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Stage < Ci::ApplicationRecord
include Importable
include Ci::HasStatus
include Gitlab::OptimisticLocking
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 6e27abb9f5b..595315f14ab 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Trigger < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Trigger < Ci::ApplicationRecord
include Presentable
belongs_to :project
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 5daf3dd192d..b645f7ee2bb 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class TriggerRequest < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class TriggerRequest < Ci::ApplicationRecord
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
has_many :builds
diff --git a/app/models/ci/unit_test.rb b/app/models/ci/unit_test.rb
index 9fddd9c6002..96b701840ea 100644
--- a/app/models/ci/unit_test.rb
+++ b/app/models/ci/unit_test.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class UnitTest < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class UnitTest < Ci::ApplicationRecord
MAX_NAME_SIZE = 255
MAX_SUITE_NAME_SIZE = 255
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index 480f9cefb8e..a5aa3b70e37 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -1,9 +1,7 @@
# frozen_string_literal: true
module Ci
- class UnitTestFailure < ApplicationRecord
- extend Gitlab::Ci::Model
-
+ class UnitTestFailure < Ci::ApplicationRecord
REPORT_WINDOW = 14.days
validates :unit_test, :build, :failed_at, presence: true
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 84505befc5c..1e91f248fc4 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Ci
- class Variable < ApplicationRecord
- extend Gitlab::Ci::Model
+ class Variable < Ci::ApplicationRecord
include Ci::HasVariable
include Presentable
include Ci::Maskable
diff --git a/app/models/ci_platform_metric.rb b/app/models/ci_platform_metric.rb
index ac4ab391bbf..db6b73b43f7 100644
--- a/app/models/ci_platform_metric.rb
+++ b/app/models/ci_platform_metric.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
-class CiPlatformMetric < ApplicationRecord
+class CiPlatformMetric < Ci::ApplicationRecord
include BulkInsertSafe
+ self.table_name = 'ci_platform_metrics'
+
PLATFORM_TARGET_MAX_LENGTH = 255
validates :recorded_at, presence: true
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 3785023c9af..993ccb33655 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.30.0'
+ VERSION = '0.31.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 8e7f526c512..6c8b4ae1139 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -3,6 +3,7 @@
class Commit
extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache
+ extend Gitlab::Utils::Override
include ActiveModel::Conversion
include Noteable
@@ -327,7 +328,7 @@ class Commit
end
def user_mentions
- CommitUserMention.where(commit_id: self.id)
+ user_mention_class.where(commit_id: self.id)
end
def discussion_notes
@@ -554,6 +555,19 @@ class Commit
Ability.allowed?(user, :read_commit, self)
end
+ override :user_mention_class
+ def user_mention_class
+ CommitUserMention
+ end
+
+ override :user_mention_identifier
+ def user_mention_identifier
+ {
+ commit_id: id,
+ note_id: nil
+ }
+ end
+
private
def expire_note_etag_cache_for_related_mrs
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cf23cd3be67..b34d64de101 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class CommitStatus < ApplicationRecord
+class CommitStatus < Ci::ApplicationRecord
include Ci::HasStatus
include Importable
include AfterCommitQueue
@@ -58,6 +58,7 @@ class CommitStatus < ApplicationRecord
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :eager_load_pipeline, -> { eager_load(:pipeline, project: { namespace: :route }) }
scope :with_pipeline, -> { joins(:pipeline) }
+ scope :updated_at_before, ->(date) { where('updated_at < ?', date) }
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 2a0274f5706..7bb6004ca83 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -10,6 +10,7 @@ module Analytics
included do
belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
+ belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true
validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
@@ -28,6 +29,9 @@ module Analytics
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) }
+
+ before_save :ensure_stage_event_hash_id
+ after_commit :cleanup_old_stage_event_hash
end
def parent=(_)
@@ -133,6 +137,20 @@ module Analytics
.id_in(label_id)
.exists?
end
+
+ def ensure_stage_event_hash_id
+ previous_stage_event_hash = stage_event_hash&.hash_sha256
+
+ if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash
+ self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code)
+ end
+ end
+
+ def cleanup_old_stage_event_hash
+ if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was
+ Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was)
+ end
+ end
end
end
end
diff --git a/app/models/concerns/any_field_validation.rb b/app/models/concerns/any_field_validation.rb
deleted file mode 100644
index 987c4e7800e..00000000000
--- a/app/models/concerns/any_field_validation.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-# This module enables a record to be valid if any field is present
-#
-# Overwrite one_of_required_fields to set one of which fields must be present
-module AnyFieldValidation
- extend ActiveSupport::Concern
-
- included do
- validate :any_field_present
- end
-
- private
-
- def any_field_present
- return unless one_of_required_fields.all? { |field| self[field].blank? }
-
- errors.add(:base, _("At least one field of %{one_of_required_fields} must be present") %
- { one_of_required_fields: one_of_required_fields })
- end
-
- def one_of_required_fields
- raise NotImplementedError
- end
-end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 79b622c8dad..44d9beff27e 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -160,6 +160,8 @@ module CacheMarkdownField
# We can only store mentions if the mentionable is a database object
return unless self.is_a?(ApplicationRecord)
+ return store_mentions_without_subtransaction! if Feature.enabled?(:store_mentions_without_subtransaction, default_enabled: :yaml)
+
refs = all_references(self.author)
references = {}
@@ -190,6 +192,29 @@ module CacheMarkdownField
true
end
+ def store_mentions_without_subtransaction!
+ identifier = user_mention_identifier
+
+ # this may happen due to notes polymorphism, so noteable_id may point to a record
+ # that no longer exists as we cannot have FK on noteable_id
+ return if identifier.blank?
+
+ refs = all_references(self.author)
+
+ references = {}
+ references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
+ references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
+ references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
+
+ if references.compact.any?
+ user_mention_class.upsert(references.merge(identifier), unique_by: identifier.compact.keys)
+ else
+ user_mention_class.delete_by(identifier)
+ end
+
+ true
+ end
+
def mentionable_attributes_changed?(changes = saved_changes)
return false unless is_a?(Mentionable)
diff --git a/app/models/concerns/cascading_namespace_setting_attribute.rb b/app/models/concerns/cascading_namespace_setting_attribute.rb
index 5d24e15d518..e58e5ddc966 100644
--- a/app/models/concerns/cascading_namespace_setting_attribute.rb
+++ b/app/models/concerns/cascading_namespace_setting_attribute.rb
@@ -127,7 +127,7 @@ module CascadingNamespaceSettingAttribute
end
def alias_boolean(attribute)
- return unless Gitlab::Database.exists? && type_for_attribute(attribute).type == :boolean
+ return unless Gitlab::Database.main.exists? && type_for_attribute(attribute).type == :boolean
alias_method :"#{attribute}?", attribute
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index f3c254053b5..c1299e3d468 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -93,6 +93,7 @@ module Ci
scope :running_or_pending, -> { with_status(:running, :pending) }
scope :finished, -> { with_status(:success, :failed, :canceled) }
scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
+ scope :complete, -> { with_status(completed_statuses) }
scope :incomplete, -> { without_statuses(completed_statuses) }
scope :cancelable, -> do
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 114435d5a21..ec86746ae54 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -76,14 +76,8 @@ module Ci
end
def write_metadata_attribute(legacy_key, metadata_key, value)
- # save to metadata or this model depending on the state of feature flag
- if Feature.enabled?(:ci_build_metadata_config, project, default_enabled: :yaml)
- ensure_metadata.write_attribute(metadata_key, value)
- write_attribute(legacy_key, nil)
- else
- write_attribute(legacy_key, value)
- metadata&.write_attribute(metadata_key, nil)
- end
+ ensure_metadata.write_attribute(metadata_key, value)
+ write_attribute(legacy_key, nil)
end
end
end
diff --git a/app/models/concerns/ci/namespaced_model_name.rb b/app/models/concerns/ci/namespaced_model_name.rb
new file mode 100644
index 00000000000..e941a3a7a0c
--- /dev/null
+++ b/app/models/concerns/ci/namespaced_model_name.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ module NamespacedModelName
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, Ci)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 829b2a6ef21..4bfeba338d2 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -128,8 +128,7 @@ module CounterAttribute
end
def counter_attribute_enabled?(attribute)
- Feature.enabled?(:efficient_counter_attribute, project) &&
- self.class.counter_attributes.include?(attribute)
+ self.class.counter_attributes.include?(attribute)
end
private
diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb
index a59f00d73ec..443e1ab53b4 100644
--- a/app/models/concerns/each_batch.rb
+++ b/app/models/concerns/each_batch.rb
@@ -91,7 +91,11 @@ module EachBatch
# Any ORDER BYs are useless for this relation and can lead to less
# efficient UPDATE queries, hence we get rid of it.
- yield relation.except(:order), index
+ relation = relation.except(:order)
+
+ # Using unscoped is necessary to prevent leaking the current scope used by
+ # ActiveRecord to chain `each_batch` method.
+ unscoped { yield relation, index }
break unless stop
end
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index c42b046592f..94d11c871ca 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -37,7 +37,9 @@ module Enums
merge_request_event: 10,
external_pull_request_event: 11,
parent_pipeline: 12,
- ondemand_dast_scan: 13
+ ondemand_dast_scan: 13,
+ ondemand_dast_validation: 14,
+ security_orchestration_policy: 15
}
end
@@ -48,8 +50,10 @@ module Enums
# parent pipeline. It's up to the parent to affect the ref CI status
# - when an ondemand_dast_scan pipeline runs it is for testing purpose and should
# not affect the ref CI status.
+ # - when an ondemand_dast_validation pipeline runs it is for validating a DAST site
+ # profile and should not affect the ref CI status.
def self.dangling_sources
- sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan)
+ sources.slice(:webide, :parent_pipeline, :ondemand_dast_scan, :ondemand_dast_validation, :security_orchestration_policy)
end
# CI sources are those pipeline events that affect the CI status of the ref
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index 512822089ba..e029ada84f0 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -13,6 +13,9 @@ module Expirable
expires? && expires_at <= Time.current
end
+ # Used in subclasses that override expired?
+ alias_method :expired_original?, :expired?
+
def expires?
expires_at.present?
end
diff --git a/app/models/concerns/has_integrations.rb b/app/models/concerns/has_integrations.rb
index 25650ae56ad..76e03d68600 100644
--- a/app/models/concerns/has_integrations.rb
+++ b/app/models/concerns/has_integrations.rb
@@ -4,18 +4,6 @@ module HasIntegrations
extend ActiveSupport::Concern
class_methods do
- def with_custom_integration_for(integration, page = nil, per = nil)
- custom_integration_project_ids = Integration
- .select(:project_id)
- .where(type: integration.type)
- .where(inherit_from_id: nil)
- .where.not(project_id: nil)
- .page(page)
- .per(per)
-
- Project.where(id: custom_integration_project_ids)
- end
-
def without_integration(integration)
integrations = Integration
.select('1')
diff --git a/app/models/concerns/incident_management/escalatable.rb b/app/models/concerns/incident_management/escalatable.rb
new file mode 100644
index 00000000000..78dce63f59e
--- /dev/null
+++ b/app/models/concerns/incident_management/escalatable.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ # Shared functionality for a `#status` field, representing
+ # whether action is required. In EE, this corresponds
+ # to paging functionality with EscalationPolicies.
+ #
+ # This module is only responsible for setting the status and
+ # possible status-related timestamps (EX triggered_at/resolved_at)
+ # for the implementing class. The relationships between these
+ # values and other related timestamps/logic should be managed from
+ # the object class itself. (EX Alert#ended_at = Alert#resolved_at)
+ module Escalatable
+ extend ActiveSupport::Concern
+
+ STATUSES = {
+ triggered: 0,
+ acknowledged: 1,
+ resolved: 2,
+ ignored: 3
+ }.freeze
+
+ STATUS_DESCRIPTIONS = {
+ triggered: 'Investigation has not started',
+ acknowledged: 'Someone is actively investigating the problem',
+ resolved: 'The problem has been addressed',
+ ignored: 'No action will be taken'
+ }.freeze
+
+ included do
+ validates :status, presence: true
+
+ # Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
+ # Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
+ scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
+
+ state_machine :status, initial: :triggered do
+ state :triggered, value: STATUSES[:triggered]
+
+ state :acknowledged, value: STATUSES[:acknowledged]
+
+ state :resolved, value: STATUSES[:resolved] do
+ validates :resolved_at, presence: true
+ end
+
+ state :ignored, value: STATUSES[:ignored]
+
+ state :triggered, :acknowledged, :ignored do
+ validates :resolved_at, absence: true
+ end
+
+ event :trigger do
+ transition any => :triggered
+ end
+
+ event :acknowledge do
+ transition any => :acknowledged
+ end
+
+ event :resolve do
+ transition any => :resolved
+ end
+
+ event :ignore do
+ transition any => :ignored
+ end
+
+ before_transition to: [:triggered, :acknowledged, :ignored] do |escalatable, _transition|
+ escalatable.resolved_at = nil
+ end
+
+ before_transition to: :resolved do |escalatable, transition|
+ resolved_at = transition.args.first
+ escalatable.resolved_at = resolved_at || Time.current
+ end
+ end
+
+ class << self
+ def status_value(name)
+ state_machine_statuses[name]
+ end
+
+ def status_name(raw_status)
+ state_machine_statuses.key(raw_status)
+ end
+
+ def status_names
+ @status_names ||= state_machine_statuses.keys
+ end
+
+ private
+
+ def state_machine_statuses
+ @state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
+ end
+ end
+
+ def status_event_for(status)
+ self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d5e2e63402f..8d0f8b01d64 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -152,7 +152,7 @@ module Issuable
participant :notes_with_associations
participant :assignees
- strip_attributes :title
+ strip_attributes! :title
class << self
def labels_hash
@@ -374,6 +374,8 @@ module Issuable
grouping_columns << milestone_table[:due_date]
elsif %w(merged_at_desc merged_at_asc).include?(sort)
grouping_columns << MergeRequest::Metrics.arel_table[:merged_at]
+ elsif %w(closed_at_desc closed_at_asc).include?(sort)
+ grouping_columns << MergeRequest::Metrics.arel_table[:closed_at]
end
grouping_columns
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
index 41efea65c5a..fab1aa21634 100644
--- a/app/models/concerns/limitable.rb
+++ b/app/models/concerns/limitable.rb
@@ -9,6 +9,7 @@ module Limitable
class_attribute :limit_relation
class_attribute :limit_name
class_attribute :limit_feature_flag
+ class_attribute :limit_feature_flag_for_override # Allows selectively disabling by actor (as per https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor)
self.limit_name = self.name.demodulize.tableize
validate :validate_plan_limit_not_exceeded, on: :create
@@ -28,6 +29,7 @@ module Limitable
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)
+ return if limit_feature_flag_for_override && ::Feature.enabled?(limit_feature_flag_for_override, scope_relation, default_enabled: :yaml)
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
limits = scope_relation.actual_limits
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index f1baa923ec5..4df9e32d8ec 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -161,6 +161,21 @@ module Mentionable
create_cross_references!(author)
end
+ def user_mention_class
+ user_mention_association.klass
+ end
+
+ # Identifier for the user mention that is parsed from model description rather then its related notes.
+ # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
+ # Other mentionable models like DesignManagement::Design, will never have such record as those do not have
+ # a description attribute.
+ def user_mention_identifier
+ {
+ user_mention_association.foreign_key => id,
+ note_id: nil
+ }
+ end
+
private
def extracted_mentionables(refs)
@@ -199,6 +214,10 @@ module Mentionable
{}
end
+ def user_mention_association
+ association(:user_mentions).reflection
+ end
+
# User mention that is parsed from model description rather then its related notes.
# Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 159f0044c82..196bec04be6 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -77,23 +77,16 @@ module Packages
validates container_type, presence: true
validates :file_store, presence: true
-
- validates :file_signature, absence: true
- validates :signing_keys, absence: true
+ validates :signed_file_store, presence: true
scope :with_container, ->(subject) { where(container_type => subject) }
scope :with_codename, ->(codename) { where(codename: codename) }
scope :with_suite, ->(suite) { where(suite: suite) }
scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
- attr_encrypted :signing_keys,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_32,
- algorithm: 'aes-256-gcm',
- encode: false,
- encode_iv: false
-
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
+ mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader
+ after_save :update_signed_file_store, if: :saved_change_to_signed_file?
def component_names
components.pluck(:name).sort
@@ -131,6 +124,12 @@ module Packages
self.class.with_container(container).with_codename(suite).exists?
end
+
+ def update_signed_file_store
+ # The signed_file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:signed_file_store, signed_file.object_store)
+ end
end
end
end
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 484c91e0833..0cab874a240 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -90,6 +90,13 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:container_registry_access_level, value)
end
+ # TODO: Remove this method after we drop support for project create/edit APIs to set the
+ # container_registry_enabled attribute. They can instead set the container_registry_access_level
+ # attribute.
+ def container_registry_enabled=(value)
+ write_feature_attribute_boolean(:container_registry_access_level, value)
+ end
+
private
def write_feature_attribute_boolean(field, value)
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
new file mode 100644
index 00000000000..587f8c35ff7
--- /dev/null
+++ b/app/models/concerns/restricted_signup.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+module RestrictedSignup
+ extend ActiveSupport::Concern
+
+ private
+
+ def validate_admin_signup_restrictions(email)
+ return if allowed_domain?(email)
+
+ if allowlist_present?
+ return _('domain is not authorized for sign-up.')
+ elsif denied_domain?(email)
+ return _('is not from an allowed domain.')
+ elsif restricted_email?(email)
+ return _('is not allowed. Try again with a different email address, or contact your GitLab admin.')
+ end
+
+ nil
+ end
+
+ def denied_domain?(email)
+ return false unless Gitlab::CurrentSettings.domain_denylist_enabled?
+
+ denied_domains = Gitlab::CurrentSettings.domain_denylist
+ denied_domains.present? && domain_matches?(denied_domains, email)
+ end
+
+ def allowlist_present?
+ Gitlab::CurrentSettings.domain_allowlist.present?
+ end
+
+ def allowed_domain?(email)
+ allowed_domains = Gitlab::CurrentSettings.domain_allowlist
+ allowlist_present? && domain_matches?(allowed_domains, email)
+ end
+
+ def restricted_email?(email)
+ return false unless Gitlab::CurrentSettings.email_restrictions_enabled?
+
+ restrictions = Gitlab::CurrentSettings.email_restrictions
+ restrictions.present? && Gitlab::UntrustedRegexp.new(restrictions).match?(email)
+ end
+
+ def domain_matches?(email_domains, email)
+ signup_domain = Mail::Address.new(email).domain
+ email_domains.any? do |domain|
+ escaped = Regexp.escape(domain).gsub('\*', '.*?')
+ regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
+ signup_domain =~ regexp
+ end
+ end
+end
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
index 4fae36f7b8d..49342e30db6 100644
--- a/app/models/concerns/select_for_project_authorization.rb
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -5,7 +5,7 @@ module SelectForProjectAuthorization
class_methods do
def select_for_project_authorization
- select("projects.id AS project_id, members.access_level")
+ select("projects.id AS project_id", "members.access_level")
end
def select_as_maintainer_for_project_authorization
diff --git a/app/models/concerns/sha256_attribute.rb b/app/models/concerns/sha256_attribute.rb
index 4921f7f1a7e..17fda6c806c 100644
--- a/app/models/concerns/sha256_attribute.rb
+++ b/app/models/concerns/sha256_attribute.rb
@@ -39,7 +39,7 @@ module Sha256Attribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index f6f5dbce4b6..27277bc5296 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -32,7 +32,7 @@ module ShaAttribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 2daea388939..4901cd832ff 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -111,7 +111,7 @@ module Spammable
end
# Override in Spammable if further checks are necessary
- def check_for_spam?
+ def check_for_spam?(user:)
true
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
index 8f6a6244dd3..1c433a3275e 100644
--- a/app/models/concerns/strip_attribute.rb
+++ b/app/models/concerns/strip_attribute.rb
@@ -7,7 +7,7 @@
# Usage:
#
# class Milestone < ApplicationRecord
-# strip_attributes :title
+# strip_attributes! :title
# end
#
#
@@ -15,7 +15,7 @@ module StripAttribute
extend ActiveSupport::Concern
class_methods do
- def strip_attributes(*attrs)
+ def strip_attributes!(*attrs)
strip_attrs.concat(attrs)
end
@@ -25,10 +25,10 @@ module StripAttribute
end
included do
- before_validation :strip_attributes
+ before_validation :strip_attributes!
end
- def strip_attributes
+ def strip_attributes!
self.class.strip_attrs.each do |attr|
self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!)
end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 89b42eec727..54fe9eac2bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -11,7 +11,7 @@ module TimeTrackable
extend ActiveSupport::Concern
included do
- attr_reader :time_spent, :time_spent_user, :spent_at
+ attr_reader :time_spent, :time_spent_user, :spent_at, :summary
alias_method :time_spent?, :time_spent
@@ -20,7 +20,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -29,6 +29,7 @@ module TimeTrackable
@time_spent_note_id = options[:note_id]
@time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
+ @summary = options[:summary]
@original_total_time_spent = nil
return if @time_spent == 0
@@ -78,7 +79,8 @@ module TimeTrackable
time_spent: time_spent,
note_id: @time_spent_note_id,
user: @time_spent_user,
- spent_at: @spent_at
+ spent_at: @spent_at,
+ summary: @summary
)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 8dc58f8dca1..79cbe225e5a 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -106,7 +106,7 @@ module Timebox
.where('due_date is NULL or due_date >= ?', start_date)
end
- strip_attributes :title
+ strip_attributes! :title
alias_attribute :name, :title
end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index f0e5e010e70..a656856487d 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -2,6 +2,35 @@
module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
-end
+ def matches_signatures(other_signatures, other_uuid)
+ other_signature_types = other_signatures.index_by(&:algorithm_type)
+
+ # highest first
+ match_result = nil
+ signatures.sort_by(&:priority).reverse_each do |signature|
+ matching_other_signature = other_signature_types[signature.algorithm_type]
+ next if matching_other_signature.nil?
+
+ match_result = matching_other_signature == signature
+ break
+ end
-VulnerabilityFindingHelpers.prepend_mod_with('VulnerabilityFindingHelpers')
+ if match_result.nil?
+ [uuid, *signature_uuids].include?(other_uuid)
+ else
+ match_result
+ end
+ end
+
+ def signature_uuids
+ signatures.map do |signature|
+ hex_sha = signature.signature_hex
+ ::Security::VulnerabilityUUID.generate(
+ report_type: report_type,
+ location_fingerprint: hex_sha,
+ primary_identifier_fingerprint: primary_identifier&.fingerprint,
+ project_id: project_id
+ )
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb
index f98c1e93aaf..71a12b4077b 100644
--- a/app/models/concerns/vulnerability_finding_signature_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb
@@ -2,6 +2,30 @@
module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
-end
+ # If the location object describes a physical location within a file
+ # (filename + line numbers), the 'location' algorithm_type should be used
+ # If the location object describes arbitrary data, then the 'hash'
+ # algorithm_type should be used.
+
+ ALGORITHM_TYPES = { hash: 1, location: 2, scope_offset: 3 }.with_indifferent_access.freeze
+
+ class_methods do
+ def priority(algorithm_type)
+ raise ArgumentError, "No priority for #{algorithm_type.inspect}" unless ALGORITHM_TYPES.key?(algorithm_type)
+
+ ALGORITHM_TYPES[algorithm_type]
+ end
-VulnerabilityFindingSignatureHelpers.prepend_mod_with('VulnerabilityFindingSignatureHelpers')
+ def algorithm_types
+ ALGORITHM_TYPES
+ end
+ end
+
+ def priority
+ self.class.priority(algorithm_type)
+ end
+
+ def algorithm_types
+ self.class.algorithm_types
+ end
+end
diff --git a/app/models/concerns/x509_serial_number_attribute.rb b/app/models/concerns/x509_serial_number_attribute.rb
index dbba80eff53..dfb1e151b41 100644
--- a/app/models/concerns/x509_serial_number_attribute.rb
+++ b/app/models/concerns/x509_serial_number_attribute.rb
@@ -39,7 +39,7 @@ module X509SerialNumberAttribute
end
def database_exists?
- Gitlab::Database.exists?
+ Gitlab::Database.main.exists?
end
end
end
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
new file mode 100644
index 00000000000..caf1cd68cc5
--- /dev/null
+++ b/app/models/customer_relations/organization.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CustomerRelations::Organization < ApplicationRecord
+ self.table_name = "customer_relations_organizations"
+
+ belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
+
+ before_validation :strip_whitespace!
+
+ enum state: {
+ inactive: 0,
+ active: 1
+ }
+
+ validates :group, presence: true
+ validates :name, presence: true
+ validates :name, uniqueness: { case_sensitive: false, scope: [:group_id] }
+ validates :name, length: { maximum: 255 }
+ validates :description, length: { maximum: 1024 }
+
+ def self.find_by_name(group_id, name)
+ where(group: group_id)
+ .where('LOWER(name) = LOWER(?)', name)
+ end
+
+ private
+
+ def strip_whitespace!
+ name&.strip!
+ end
+end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 5fa9f2ef9f9..326d3fb8470 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -10,6 +10,7 @@ class DeployToken < ApplicationRecord
AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
read_package_registry write_package_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
+ REQUIRED_DEPENDENCY_PROXY_SCOPES = %i[read_registry write_registry].freeze
default_value_for(:expires_at) { Forever.date }
@@ -46,6 +47,12 @@ class DeployToken < ApplicationRecord
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
end
+ def valid_for_dependency_proxy?
+ group_type? &&
+ active? &&
+ REQUIRED_DEPENDENCY_PROXY_SCOPES.all? { |scope| scope.in?(scopes) }
+ end
+
def revoke!
update!(revoked: true)
end
@@ -73,6 +80,14 @@ class DeployToken < ApplicationRecord
holder.has_access_to?(requested_project)
end
+ def has_access_to_group?(requested_group)
+ return false unless active?
+ return false unless group_type?
+ return false unless holder
+
+ holder.has_access_to_group?(requested_group)
+ end
+
# This is temporal. Currently we limit DeployToken
# to a single project or group, later we're going to
# extend that to be for multiple projects and namespaces.
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 313aeb1eda7..4a690ccc67e 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -289,7 +289,7 @@ class Deployment < ApplicationRecord
"#{id} as deployment_id",
"#{environment_id} as environment_id").to_sql
- # We don't use `Gitlab::Database.bulk_insert` here so that we don't need to
+ # We don't use `Gitlab::Database.main.bulk_insert` here so that we don't need to
# first pluck lots of IDs into memory.
#
# We also ignore any duplicates so this method can be called multiple times
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
index ca65cf38f0d..6cda03557d1 100644
--- a/app/models/design_management/version.rb
+++ b/app/models/design_management/version.rb
@@ -88,7 +88,7 @@ module DesignManagement
rows = design_actions.map { |action| action.row_attrs(version) }
- Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(::DesignManagement::Action.table_name, rows) # rubocop:disable Gitlab/BulkInsert
version.designs.reset
version.validate!
design_actions.each(&:performed)
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 642e93f7912..f4d665cf279 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -43,9 +43,13 @@ class DiffDiscussion < Discussion
end
def cache_key
+ positions_json = diff_note_positions.map { |dnp| dnp.position.to_json }
+ positions_sha = Digest::SHA1.hexdigest(positions_json.join(':')) if positions_json.any?
+
[
super,
- Digest::SHA1.hexdigest(position.to_json)
+ Digest::SHA1.hexdigest(position.to_json),
+ positions_sha
].join(':')
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 076d8cc280c..203e14f1227 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -163,16 +163,15 @@ class Discussion
end
def cache_key
- # Need this so cache will be invalidated when note within a discussion
- # has been deleted.
- notes_sha = Digest::SHA1.hexdigest(notes.map(&:id).join(':'))
+ # Need to use the notes' cache key so cache will be invalidated when note
+ # within a discussion has been deleted or has different data after post
+ # processing of content.
+ notes_sha = Digest::SHA1.hexdigest(notes.map(&:post_processed_cache_key).join(':'))
[
CACHE_VERSION,
- notes.last.latest_cached_markdown_version,
id,
notes_sha,
- notes.max_by(&:updated_at).updated_at,
resolved_at
].join(':')
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 558963c98c4..963249c018a 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -5,6 +5,7 @@ class Environment < ApplicationRecord
include ReactiveCaching
include FastDestroyAll::Helpers
include Presentable
+ include NullifyIfBlank
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
@@ -14,6 +15,7 @@ class Environment < ApplicationRecord
belongs_to :project, required: true
use_fast_destroy :all_deployments
+ nullify_if_blank :external_url
has_many :all_deployments, class_name: 'Deployment'
has_many :deployments, -> { visible }
@@ -33,7 +35,6 @@ class Environment < ApplicationRecord
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
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
before_save :set_environment_type
@@ -77,6 +78,7 @@ class Environment < ApplicationRecord
scope :for_name, -> (name) { where(name: name) }
scope :preload_cluster, -> { preload(last_deployment: :cluster) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
+ scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
##
# Search environments which have names like the given query.
@@ -230,10 +232,6 @@ class Environment < ApplicationRecord
ref.to_s == last_deployment.try(:ref)
end
- def nullify_external_url
- self.external_url = nil if self.external_url.blank?
- end
-
def set_environment_type
names = name.split('/')
diff --git a/app/models/error_tracking/client_key.rb b/app/models/error_tracking/client_key.rb
new file mode 100644
index 00000000000..9d12c0ed6f1
--- /dev/null
+++ b/app/models/error_tracking/client_key.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class ErrorTracking::ClientKey < ApplicationRecord
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :public_key, presence: true, length: { maximum: 255 }
+
+ scope :active, -> { where(active: true) }
+
+ after_initialize :generate_key
+
+ def self.find_by_public_key(key)
+ find_by(public_key: key)
+ end
+
+ private
+
+ def generate_key
+ self.public_key = "glet_#{SecureRandom.hex}"
+ end
+end
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 012dcc4418f..32932c4d045 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -5,10 +5,19 @@ class ErrorTracking::Error < ApplicationRecord
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
+ scope :for_status, -> (status) { where(status: status) }
+
validates :project, presence: true
validates :name, presence: true
validates :description, presence: true
validates :actor, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ unresolved: 0,
+ resolved: 1,
+ ignored: 2
+ }
def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by(
@@ -20,4 +29,64 @@ class ErrorTracking::Error < ApplicationRecord
error.update!(last_seen_at: timestamp)
end
end
+
+ def title
+ if description.present?
+ "#{name} #{description}"
+ else
+ name
+ end
+ end
+
+ def title_truncated
+ title.truncate(64)
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error
+ Gitlab::ErrorTracking::Error.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at,
+ last_seen: last_seen_at,
+ status: status,
+ count: events_count
+ )
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_detailed_error
+ Gitlab::ErrorTracking::DetailedError.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at.to_s,
+ last_seen: last_seen_at.to_s,
+ count: events_count,
+ user_count: 0, # we don't support user count yet.
+ project_id: project.id,
+ status: status,
+ tags: { level: nil, logger: nil },
+ external_url: external_url,
+ external_base_url: external_base_url
+ )
+ end
+
+ private
+
+ # For compatibility with sentry integration
+ def external_url
+ Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url(
+ namespace_id: project.namespace,
+ project_id: project,
+ issue_id: id)
+ end
+
+ # For compatibility with sentry integration
+ def external_base_url
+ Gitlab::Routing.url_helpers.root_url
+ end
end
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index ed14a1bce41..4de13de7e2e 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -8,4 +8,69 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :error, presence: true
validates :description, presence: true
validates :occurred_at, presence: true
+
+ def stacktrace
+ @stacktrace ||= build_stacktrace
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error_event
+ Gitlab::ErrorTracking::ErrorEvent.new(
+ issue_id: error_id,
+ date_received: occurred_at,
+ stack_trace_entries: stacktrace
+ )
+ end
+
+ private
+
+ def build_stacktrace
+ raw_stacktrace = find_stacktrace_from_payload
+
+ return [] unless raw_stacktrace
+
+ raw_stacktrace.map do |entry|
+ {
+ 'lineNo' => entry['lineno'],
+ 'context' => build_stacktrace_context(entry),
+ 'filename' => entry['filename'],
+ 'function' => entry['function'],
+ 'colNo' => 0 # we don't support colNo yet.
+ }
+ end
+ end
+
+ def find_stacktrace_from_payload
+ exception_entry = payload.dig('exception')
+
+ if exception_entry
+ exception_values = exception_entry.dig('values')
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ stack_trace_entry&.dig('stacktrace', 'frames')
+ end
+ end
+
+ def build_stacktrace_context(entry)
+ context = []
+ error_line = entry['context_line']
+ error_line_no = entry['lineno']
+ pre_context = entry['pre_context']
+ post_context = entry['post_context']
+
+ context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position([error_line], error_line_no)
+ context += lines_with_position(post_context, error_line_no + 1)
+
+ context.reject(&:blank?)
+ end
+
+ def lines_with_position(lines, position)
+ return [] if lines.blank?
+
+ lines.map.with_index do |line, index|
+ next unless line
+
+ [position + index, line]
+ end
+ end
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index c729b002852..c5a77427588 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -31,12 +31,13 @@ module ErrorTracking
validates :api_url, length: { maximum: 255 }, public_url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
validates :enabled, inclusion: { in: [true, false] }
+ validates :integrated, inclusion: { in: [true, false] }
- validates :api_url, presence: { message: 'is a required field' }, if: :enabled
-
- validate :validate_api_url_path, if: :enabled
-
- validates :token, presence: { message: 'is a required field' }, if: :enabled
+ with_options if: :sentry_enabled do
+ validates :api_url, presence: { message: 'is a required field' }
+ validates :token, presence: { message: 'is a required field' }
+ validate :validate_api_url_path
+ end
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -45,6 +46,14 @@ module ErrorTracking
after_save :clear_reactive_cache!
+ def sentry_enabled
+ enabled && !integrated_client?
+ end
+
+ def integrated_client?
+ integrated && ::Feature.enabled?(:integrated_error_tracking, project)
+ end
+
def api_url=(value)
super
clear_memoization(:api_url_slugs)
@@ -79,7 +88,7 @@ module ErrorTracking
def sentry_client
strong_memoize(:sentry_client) do
- ErrorTracking::SentryClient.new(api_url, token)
+ ::ErrorTracking::SentryClient.new(api_url, token)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 14d20b0d6c4..f6174589a84 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -434,9 +434,9 @@ class Event < ApplicationRecord
def design_action_names
{
- created: _('uploaded'),
- updated: _('revised'),
- destroyed: _('deleted')
+ created: _('added'),
+ updated: _('updated'),
+ destroyed: _('removed')
}
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1e7308499a0..f6b45a755e4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -80,7 +80,7 @@ class Group < Namespace
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- delegate :prevent_sharing_groups_outside_hierarchy, to: :namespace_settings
+ delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, to: :namespace_settings
accepts_nested_attributes_for :variables, allow_destroy: true
@@ -158,7 +158,7 @@ class Group < Namespace
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 })
- .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
+ .select("projects.id AS project_id", "LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
@@ -296,7 +296,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Groups::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
@@ -306,7 +306,7 @@ class Group < Namespace
end
def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
- Members::Groups::CreatorService.new(self, # rubocop:todo CodeReuse/ServiceClass
+ Members::Groups::CreatorService.new(self, # rubocop:disable CodeReuse/ServiceClass
user,
access_level,
current_user: current_user,
@@ -463,7 +463,7 @@ class Group < Namespace
id
end
- group_hierarchy_members = GroupMember.where(source_id: source_ids)
+ group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
GroupMember.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares]).authorizable
@@ -481,6 +481,7 @@ class Group < Namespace
group_hierarchy_members = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: source_ids)
+ .select(*GroupMember.cached_column_list)
GroupMember.from_union([group_hierarchy_members,
members_from_self_and_ancestor_group_shares])
@@ -729,6 +730,10 @@ class Group < Namespace
end
# rubocop: enable CodeReuse/ServiceClass
+ def timelogs
+ Timelog.in_group(self)
+ end
+
private
def max_member_access(user_ids)
diff --git a/app/models/group_deploy_token.rb b/app/models/group_deploy_token.rb
index 084a8672460..d9667e7c74d 100644
--- a/app/models/group_deploy_token.rb
+++ b/app/models/group_deploy_token.rb
@@ -11,9 +11,14 @@ class GroupDeployToken < ApplicationRecord
def has_access_to?(requested_project)
requested_project_group = requested_project&.group
return false unless requested_project_group
- return true if requested_project_group.id == group_id
- requested_project_group
+ has_access_to_group?(requested_project_group)
+ end
+
+ def has_access_to_group?(requested_group)
+ return true if requested_group.id == group_id
+
+ requested_group
.ancestors
.where(id: group_id)
.exists?
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 5f8fa4bca0a..9a78fe3971c 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -69,21 +69,26 @@ class WebHook < ApplicationRecord
end
def disable!
- update!(recent_failures: FAILURE_THRESHOLD + 1)
+ update_attribute(:recent_failures, FAILURE_THRESHOLD + 1)
end
def enable!
return if recent_failures == 0 && disabled_until.nil? && backoff_count == 0
- update!(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ assign_attributes(recent_failures: 0, disabled_until: nil, backoff_count: 0)
+ save(validate: false)
end
def backoff!
- update!(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ save(validate: false)
end
def failed!
- update!(recent_failures: recent_failures + 1) if recent_failures < MAX_FAILURES
+ return unless recent_failures < MAX_FAILURES
+
+ assign_attributes(recent_failures: recent_failures + 1)
+ save(validate: false)
end
# Overridden in ProjectHook and GroupHook, other webhooks are not rate-limited.
diff --git a/app/models/incident_management/issuable_escalation_status.rb b/app/models/incident_management/issuable_escalation_status.rb
new file mode 100644
index 00000000000..88aef104d88
--- /dev/null
+++ b/app/models/incident_management/issuable_escalation_status.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class IssuableEscalationStatus < ApplicationRecord
+ include ::IncidentManagement::Escalatable
+
+ self.table_name = 'incident_management_issuable_escalation_statuses'
+
+ belongs_to :issue
+
+ validates :issue, presence: true, uniqueness: true
+ end
+end
+
+IncidentManagement::IssuableEscalationStatus.prepend_mod_with('IncidentManagement::IssuableEscalationStatus')
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index f401c23e453..09a60e9dd10 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -13,7 +13,9 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
- gitlab_ci: gitlab_ci }.deep_symbolize_keys
+ gitlab_ci: gitlab_ci,
+ package_file_size_limits: package_file_size_limits,
+ rate_limits: rate_limits }.deep_symbolize_keys
end
end
@@ -43,6 +45,66 @@ class InstanceConfiguration
default: 100.megabytes })
end
+ def package_file_size_limits
+ Plan.all.to_h { |plan| [plan.name.capitalize, plan_file_size_limits(plan)] }
+ end
+
+ def plan_file_size_limits(plan)
+ {
+ conan: plan.actual_limits[:conan_max_file_size],
+ maven: plan.actual_limits[:maven_max_file_size],
+ npm: plan.actual_limits[:npm_max_file_size],
+ nuget: plan.actual_limits[:nuget_max_file_size],
+ pypi: plan.actual_limits[:pypi_max_file_size],
+ terraform_module: plan.actual_limits[:terraform_module_max_file_size],
+ generic: plan.actual_limits[:generic_packages_max_file_size]
+ }
+ end
+
+ def rate_limits
+ {
+ unauthenticated: {
+ enabled: application_settings[:throttle_unauthenticated_enabled],
+ requests_per_period: application_settings[:throttle_unauthenticated_requests_per_period],
+ period_in_seconds: application_settings[:throttle_unauthenticated_period_in_seconds]
+ },
+ authenticated_api: {
+ enabled: application_settings[:throttle_authenticated_api_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_api_period_in_seconds]
+ },
+ authenticated_web: {
+ enabled: application_settings[:throttle_authenticated_web_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_web_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_web_period_in_seconds]
+ },
+ protected_paths: {
+ enabled: application_settings[:throttle_protected_paths_enabled],
+ requests_per_period: application_settings[:throttle_protected_paths_requests_per_period],
+ period_in_seconds: application_settings[:throttle_protected_paths_period_in_seconds]
+ },
+ unauthenticated_packages_api: {
+ enabled: application_settings[:throttle_unauthenticated_packages_api_enabled],
+ requests_per_period: application_settings[:throttle_unauthenticated_packages_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_unauthenticated_packages_api_period_in_seconds]
+ },
+ authenticated_packages_api: {
+ enabled: application_settings[:throttle_authenticated_packages_api_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_packages_api_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_packages_api_period_in_seconds]
+ },
+ issue_creation: application_setting_limit_per_minute(:issues_create_limit),
+ note_creation: application_setting_limit_per_minute(:notes_create_limit),
+ project_export: application_setting_limit_per_minute(:project_export_limit),
+ project_export_download: application_setting_limit_per_minute(:project_download_export_limit),
+ project_import: application_setting_limit_per_minute(:project_import_limit),
+ group_export: application_setting_limit_per_minute(:group_export_limit),
+ group_export_download: application_setting_limit_per_minute(:group_download_export_limit),
+ group_import: application_setting_limit_per_minute(:group_import_limit),
+ raw_blob: application_setting_limit_per_minute(:raw_blob_request_limit)
+ }
+ end
+
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
@@ -70,4 +132,16 @@ class InstanceConfiguration
def ssh_algorithm_sha256(ssh_file_content)
Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end
+
+ def application_settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
+ def application_setting_limit_per_minute(setting)
+ {
+ enabled: application_settings[setting] > 0,
+ requests_per_period: application_settings[setting],
+ period_in_seconds: 1.minute
+ }
+ end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index ea1e3840f6c..a9c865569d0 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -62,15 +62,13 @@ class Integration < ApplicationRecord
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_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 :project_id, presence: true, unless: -> { instance_level? || group_level? }
+ validates :group_id, presence: true, unless: -> { instance_level? || project_level? }
+ validates :project_id, :group_id, absence: true, if: -> { instance_level? }
validates :type, presence: true, exclusion: BASE_CLASSES
- validates :type, uniqueness: { scope: :template }, if: :template?
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
scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
@@ -79,9 +77,9 @@ class Integration < ApplicationRecord
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
- scope :inherit, -> { where.not(inherit_from_id: nil) }
+ scope :with_default_settings, -> { where.not(inherit_from_id: nil) }
+ scope :with_custom_settings, -> { where(inherit_from_id: nil) }
scope :for_group, -> (group) { where(group_id: group, type: available_integration_types(include_project_specific: false)) }
- scope :for_template, -> { where(template: true, type: available_integration_types(include_project_specific: false)) }
scope :for_instance, -> { where(instance: true, type: available_integration_types(include_project_specific: false)) }
scope :push_hooks, -> { where(push_events: true, active: true) }
@@ -169,25 +167,10 @@ class Integration < ApplicationRecord
'push'
end
- def self.find_or_create_templates
- create_nonexistent_templates
- for_template
+ def self.event_description(event)
+ IntegrationsHelper.integration_event_description(event)
end
- def self.create_nonexistent_templates
- nonexistent_integrations = build_nonexistent_integrations_for(for_template)
- return if nonexistent_integrations.empty?
-
- # Create within a transaction to perform the lowest possible SQL queries.
- transaction do
- nonexistent_integrations.each do |integration|
- integration.template = true
- integration.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)
return unless name.in?(available_integration_names(include_project_specific: false))
@@ -275,7 +258,6 @@ class Integration < ApplicationRecord
data_fields.integration = new_integration
end
- new_integration.template = false
new_integration.instance = false
new_integration.project_id = project_id
new_integration.group_id = group_id
@@ -292,7 +274,7 @@ class Integration < ApplicationRecord
end
def self.closest_group_integration(type, scope)
- group_ids = scope.ancestors.select(:id)
+ group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
@@ -306,12 +288,11 @@ class Integration < ApplicationRecord
end
private_class_method :instance_level_integration
- def self.create_from_active_default_integrations(scope, association, with_templates: false)
+ def self.create_from_active_default_integrations(scope, association)
group_ids = sorted_ancestors(scope).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
from_union([
- with_templates ? active.where(template: true) : none,
active.where(instance: true),
active.where(group_id: group_ids, inherit_from_id: nil)
]).order(Arel.sql("type ASC, array_position(#{array}::bigint[], #{table_name}.group_id), instance DESC")).group_by(&:type).each do |type, records|
@@ -384,7 +365,7 @@ class Integration < ApplicationRecord
end
def to_integration_hash
- as_json(methods: :type, except: %w[id template instance project_id group_id])
+ as_json(methods: :type, except: %w[id instance project_id group_id])
end
def to_data_fields_hash
@@ -503,10 +484,6 @@ class Integration < ApplicationRecord
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_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_level? && group_level?
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 590be52151c..1a7cbaa34c7 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -18,7 +18,7 @@ module Integrations
attr_accessor :response
- before_update :reset_password
+ before_validation :reset_password
def reset_password
if bamboo_url_changed? && !password_touched?
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 27c2fcf266b..5516e6bc2c0 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -2,6 +2,7 @@
module Integrations
class Datadog < Integration
+ include ActionView::Helpers::UrlHelper
include HasWebHook
extend Gitlab::Utils::Override
@@ -47,11 +48,12 @@ module Integrations
end
def description
- 'Trace your GitLab pipelines with Datadog'
+ s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
def help
- nil
+ docs_link = link_to s_('DatadogIntegration|How do I set up this integration?'), Rails.application.routes.url_helpers.help_page_url('integration/datadog'), target: '_blank', rel: 'noopener noreferrer'
+ s_('DatadogIntegration|Send CI/CD pipeline information to Datadog to monitor for job failures and troubleshoot performance issues. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
@@ -64,14 +66,19 @@ module Integrations
type: 'text',
name: 'datadog_site',
placeholder: DEFAULT_DOMAIN,
- help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site',
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe
+ },
required: false
},
{
type: 'text',
name: 'api_url',
- title: 'API URL',
- help: '(Advanced) Define the full URL for your Datadog site directly',
+ title: s_('DatadogIntegration|API URL'),
+ help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'),
required: false
},
{
@@ -80,21 +87,34 @@ module Integrations
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",
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
+ ) % {
+ linkOpen: '<a href="%s" target="_blank" rel="noopener noreferrer">'.html_safe % api_keys_url,
+ linkClose: '</a>'.html_safe
+ },
required: true
},
{
type: 'text',
name: 'datadog_service',
- title: 'Service',
+ title: s_('DatadogIntegration|Service'),
placeholder: 'gitlab-ci',
- help: 'Name of this GitLab instance that all data will be tagged with'
+ help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.')
},
{
type: 'text',
name: 'datadog_env',
- title: 'Env',
- help: 'The environment tag that traces will be tagged with'
+ title: s_('DatadogIntegration|Environment'),
+ placeholder: 'ci',
+ help: ERB::Util.html_escape(
+ s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
}
]
end
@@ -123,18 +143,18 @@ module Integrations
object_kind = 'job' if object_kind == 'build'
return unless supported_events.include?(object_kind)
+ data = data.with_retried_builds if data.respond_to?(:with_retried_builds)
+
execute_web_hook!(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] }
+ result = execute(data)
+
+ {
+ success: (200..299).cover?(result[:http_status]),
+ result: result[:message]
+ }
end
private
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index 7048dd641ea..cea4aa2038d 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -4,6 +4,8 @@ require 'uri'
module Integrations
class Irker < Integration
+ include ActionView::Helpers::UrlHelper
+
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
@@ -12,11 +14,11 @@ module Integrations
before_validation :get_channels
def title
- 'Irker (IRC gateway)'
+ s_('IrkerService|irker (IRC gateway)')
end
def description
- 'Send IRC messages.'
+ s_('IrkerService|Send update messages to an irker server.')
end
def self.to_param
@@ -42,33 +44,25 @@ module Integrations
end
def fields
+ recipients_docs_link = link_to s_('IrkerService|How to enter channels or users?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/irker', anchor: 'enter-irker-recipients'), target: '_blank', rel: 'noopener noreferrer'
[
- { type: 'text', name: 'server_host', placeholder: 'localhost',
- help: 'Irker daemon hostname (defaults to localhost)' },
- { type: 'text', name: 'server_port', placeholder: 6659,
- help: 'Irker daemon port (defaults to 6659)' },
- { type: 'text', name: 'default_irc_uri', title: 'Default IRC URI',
- help: 'A default IRC URI to prepend before each recipient (optional)',
+ { type: 'text', name: 'server_host', placeholder: 'localhost', title: s_('IrkerService|Server host (optional)'),
+ help: s_('IrkerService|irker daemon hostname (defaults to localhost).') },
+ { type: 'text', name: 'server_port', placeholder: 6659, title: s_('IrkerService|Server port (optional)'),
+ help: s_('IrkerService|irker daemon port (defaults to 6659).') },
+ { type: 'text', name: 'default_irc_uri', title: s_('IrkerService|Default IRC URI (optional)'),
+ help: s_('IrkerService|URI to add before each recipient.'),
placeholder: 'irc://irc.network.net:6697/' },
- { type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces', required: true,
- help: 'Recipients have to be specified with a full URI: '\
- 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
- 'you want the channel to be a nickname instead, append ",isnick" to ' \
- 'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
- ' want to use a password, you have to omit the "#" on the channel). If you ' \
- ' specify a default IRC URI to prepend before each recipient, you can just ' \
- ' give a channel name.' },
- { type: 'checkbox', name: 'colorize_messages' }
+ { type: 'textarea', name: 'recipients', title: s_('IrkerService|Recipients'),
+ placeholder: 'irc[s]://irc.network.net[:port]/#channel', required: true,
+ help: s_('IrkerService|Channels and users separated by whitespaces. %{recipients_docs_link}').html_safe % { recipients_docs_link: recipients_docs_link.html_safe } },
+ { type: 'checkbox', name: 'colorize_messages', title: _('Colorize messages') }
]
end
def help
- ' NOTE: Irker does NOT have built-in authentication, which makes it' \
- ' vulnerable to spamming IRC channels if it is hosted outside of a ' \
- ' firewall. Please make sure you run the daemon within a secured network ' \
- ' to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.'
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/irker', anchor: 'set-up-an-irker-daemon'), target: '_blank', rel: 'noopener noreferrer'
+ s_('IrkerService|Send update messages to an irker server. Before you can use this, you need to set up the irker daemon. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
private
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 55fc60990f3..e5c1d5ad0d7 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -8,7 +8,7 @@ module Integrations
prop_accessor :jenkins_url, :project_name, :username, :password
- before_update :reset_password
+ before_validation :reset_password
validates :jenkins_url, presence: true, addressable_url: true, if: :activated?
validates :project_name, presence: true, if: :activated?
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 1dc5c0db9e3..ec6adc87bf4 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -33,7 +33,7 @@ module Integrations
data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
:vulnerabilities_enabled, :vulnerabilities_issuetype
- before_update :reset_password
+ before_validation :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
@@ -65,7 +65,10 @@ module Integrations
end
def reset_password
- data_fields.password = nil if reset_password?
+ return unless reset_password?
+
+ data_fields.password = nil
+ properties.delete('password') if properties
end
def set_default_data
@@ -536,8 +539,7 @@ module Integrations
end
def update_deployment_type?
- (api_url_changed? || url_changed? || username_changed? || password_changed?) &&
- testable?
+ api_url_changed? || url_changed? || username_changed? || password_changed?
end
def update_deployment_type
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 91e6800f03c..5aad25e8ddc 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -15,7 +15,7 @@ module Integrations
end
def help
- '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html">How do I configure this integration?</a></p>'
+ '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
def webhook_placeholder
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index fb0917db02b..f616bc5faf2 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -18,7 +18,7 @@ module Integrations
end
def description
- s_('Integrations|Update your Packagist projects.')
+ s_('Integrations|Keep your PHP dependencies updated on Packagist.')
end
def self.to_param
@@ -27,9 +27,30 @@ module Integrations
def fields
[
- { type: 'text', name: 'username', placeholder: '', required: true },
- { type: 'text', name: 'token', placeholder: '', required: true },
- { type: 'text', name: 'server', placeholder: 'https://packagist.org', required: false }
+ {
+ type: 'text',
+ name: 'username',
+ title: _('Username'),
+ help: s_('Enter your Packagist username.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'token',
+ title: _('Token'),
+ help: s_('Enter your Packagist token.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'server',
+ title: _('Server (optional)'),
+ help: s_('Enter your Packagist server. Defaults to https://packagist.org.'),
+ placeholder: 'https://packagist.org',
+ required: false
+ }
]
end
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index b0cadc7ef4e..db39a4c68bd 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -21,18 +21,46 @@ module Integrations
def fields
[
- { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true },
- { type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true },
- { type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') },
- { type: 'select', name: 'priority', required: true, choices:
+ {
+ type: 'text',
+ name: 'api_key',
+ title: _('API key'),
+ help: s_('PushoverService|Enter your application key.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'user_key',
+ title: _('User key'),
+ help: s_('PushoverService|Enter your user key.'),
+ placeholder: '',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'device',
+ title: _('Devices (optional)'),
+ help: s_('PushoverService|Leave blank for all active devices.'),
+ placeholder: ''
+ },
+ {
+ type: 'select',
+ name: 'priority',
+ required: true,
+ choices:
[
- [s_('PushoverService|Lowest Priority'), -2],
- [s_('PushoverService|Low Priority'), -1],
- [s_('PushoverService|Normal Priority'), 0],
- [s_('PushoverService|High Priority'), 1]
+ [s_('PushoverService|Lowest priority'), -2],
+ [s_('PushoverService|Low priority'), -1],
+ [s_('PushoverService|Normal priority'), 0],
+ [s_('PushoverService|High priority'), 1]
],
- default_choice: 0 },
- { type: 'select', name: 'sound', choices:
+ default_choice: 0
+ },
+ {
+ type: 'select',
+ name: 'sound',
+ choices:
[
['Device default sound', nil],
['Pushover (default)', 'pushover'],
@@ -57,7 +85,8 @@ module Integrations
['Pushover Echo (long)', 'echo'],
['Up Down (long)', 'updown'],
['None (silent)', 'none']
- ] }
+ ]
+ }
]
end
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 135c304b57e..3f868b57597 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -18,7 +18,7 @@ module Integrations
attr_accessor :response
- before_update :reset_password
+ before_validation :reset_password
class << self
def to_param
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 834222834e9..ad6a9164d00 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -18,7 +18,7 @@ module Integrations
'This service sends notifications about projects events to a Unify Circuit conversation.<br />
To set up this service:
<ol>
- <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
+ <li><a href="https://www.circuit.com/unifyportalfaqdetail?articleId=164448" target="_blank" rel="noopener noreferrer">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>'
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index f114094d69c..a54de3c82d1 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -83,7 +83,7 @@ class InternalId < ApplicationRecord
self.internal_id_transactions_total.increment(
operation: operation,
usage: usage.to_s,
- in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s
+ in_transaction: ActiveRecord::Base.connection.transaction_open?.to_s # rubocop: disable Database/MultipleDatabases
)
end
@@ -317,7 +317,7 @@ class InternalId < ApplicationRecord
stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
- ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value')
+ ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases
end
def create_record!(subject, scope, usage, init)
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
index 35d03a544bd..928301e1da6 100644
--- a/app/models/issuable_severity.rb
+++ b/app/models/issuable_severity.rb
@@ -10,6 +10,14 @@ class IssuableSeverity < ApplicationRecord
critical: 'Critical - S1'
}.freeze
+ SEVERITY_QUICK_ACTION_PARAMS = {
+ unknown: %w(Unknown 0),
+ low: %w(Low S4 4),
+ medium: %w(Medium S3 3),
+ high: %w(High S2 2),
+ critical: %w(Critical S1 1)
+ }.freeze
+
belongs_to :issue
validates :issue, presence: true, uniqueness: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d91d72e1fba..48e3fdd51e9 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -48,6 +48,7 @@ class Issue < ApplicationRecord
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
belongs_to :iteration, foreign_key: 'sprint_id'
+ belongs_to :work_item_type, class_name: 'WorkItem::Type', inverse_of: :work_items
belongs_to :moved_to, class_name: 'Issue'
has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
@@ -76,6 +77,7 @@ class Issue < ApplicationRecord
has_one :issuable_severity
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
+ has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events
@@ -86,12 +88,7 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
- enum issue_type: {
- issue: 0,
- incident: 1,
- test_case: 2, ## EE-only
- requirement: 3 ## EE-only
- }
+ enum issue_type: WorkItem::Type.base_types
alias_method :issuing_parent, :project
@@ -134,6 +131,15 @@ class Issue < ApplicationRecord
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
+ scope :without_hidden, -> {
+ if Feature.enabled?(:ban_user_feature_flag)
+ where(id: joins('LEFT JOIN banned_users ON banned_users.user_id = issues.author_id WHERE banned_users.user_id IS NULL')
+ .select('issues.id'))
+ else
+ all
+ end
+ }
+
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
@@ -317,6 +323,21 @@ class Issue < ApplicationRecord
)
end
+ def self.to_branch_name(*args)
+ branch_name = args.map(&:to_s).each_with_index.map do |arg, i|
+ arg.parameterize(preserve_case: i == 0).presence
+ end.compact.join('-')
+
+ if branch_name.length > 100
+ truncated_string = branch_name[0, 100]
+ # Delete everything dangling after the last hyphen so as not to risk
+ # existence of unintended words in the branch name due to mid-word split.
+ branch_name = truncated_string.sub(/-[^-]*\Z/, '')
+ end
+
+ branch_name
+ 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!
@@ -384,16 +405,7 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- branch_name = "#{iid}-#{title.parameterize}"
-
- if branch_name.length > 100
- truncated_string = branch_name[0, 100]
- # Delete everything dangling after the last hyphen so as not to risk
- # existence of unintended words in the branch name due to mid-word split.
- branch_name = truncated_string[0, truncated_string.rindex("-")]
- end
-
- branch_name
+ self.class.to_branch_name(iid, title)
end
end
@@ -437,10 +449,10 @@ class Issue < ApplicationRecord
user, project.external_authorization_classification_label)
end
- def check_for_spam?
+ def check_for_spam?(user:)
# content created via support bots is always checked for spam, EVEN if
# the issue is not publicly visible and/or confidential
- return true if author.support_bot? && spammable_attribute_changed?
+ return true if user.support_bot? && spammable_attribute_changed?
# Only check for spam on issues which are publicly visible (and thus indexed in search engines)
return false unless publicly_visible?
@@ -549,6 +561,8 @@ class Issue < ApplicationRecord
true
elsif confidential? && !assignee_or_author?(user)
project.team.member?(user, Gitlab::Access::REPORTER)
+ elsif hidden?
+ false
else
project.public? ||
project.internal? && !user.external? ||
@@ -556,6 +570,10 @@ class Issue < ApplicationRecord
end
end
+ def hidden?
+ author&.banned?
+ end
+
private
def spammable_attribute_changed?
@@ -583,7 +601,7 @@ class Issue < ApplicationRecord
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
- project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
+ project.public? && !confidential? && !hidden? && !::Gitlab::ExternalAuthorization.enabled?
end
def expire_etag_cache
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 7480800abc3..759d44fb29e 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -11,6 +11,7 @@ class JiraConnectInstallation < ApplicationRecord
validates :client_key, presence: true, uniqueness: true
validates :shared_secret, presence: true
validates :base_url, presence: true, public_url: true
+ validates :instance_url, public_url: true, allow_blank: true
scope :for_project, -> (project) {
distinct
diff --git a/app/models/label.rb b/app/models/label.rb
index 1a07620f944..a46d6bc5c0f 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -9,10 +9,6 @@ class Label < ApplicationRecord
include Sortable
include FromUnion
include Presentable
- include IgnorableColumns
-
- # TODO: Project#create_labels can remove column exception when this column is dropped from all envs
- ignore_column :remove_on_close, remove_with: '14.1', remove_after: '2021-06-22'
cache_markdown_field :description, pipeline: :single_line
diff --git a/app/models/member.rb b/app/models/member.rb
index 14c886e3ab8..397e60be3a8 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -12,6 +12,7 @@ class Member < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include FromUnion
include UpdateHighestRole
+ include RestrictedSignup
AVATAR_SIZE = 40
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -42,6 +43,7 @@ class Member < ApplicationRecord
scope: [:source_type, :source_id],
allow_nil: true
}
+ validate :signup_email_valid?, on: :create, if: ->(member) { member.invite_email.present? }
validates :user_id,
uniqueness: {
message: _('project bots cannot be added to other groups / projects')
@@ -166,7 +168,7 @@ class Member < ApplicationRecord
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
- before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
+ before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? && !member.invite_accepted_at? }
after_create :send_invite, if: :invite?, unless: :importing?
after_create :send_request, if: :request?, unless: :importing?
@@ -175,7 +177,9 @@ class Member < ApplicationRecord
after_update :post_update_hook, unless: [:pending?, :importing?], if: :hook_prerequisites_met?
after_destroy :destroy_notification_setting
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
- after_commit :refresh_member_authorized_projects
+ after_save :log_invitation_token_cleanup
+
+ after_commit :refresh_member_authorized_projects, unless: :importing?
default_value_for :notification_level, NotificationSetting.levels[:global]
@@ -391,11 +395,6 @@ class Member < ApplicationRecord
# error or not doing any meaningful work.
# rubocop: disable CodeReuse/ServiceClass
def refresh_member_authorized_projects
- # If user/source is being destroyed, project access are going to be
- # destroyed eventually because of DB foreign keys, so we shouldn't bother
- # with refreshing after each member is destroyed through association
- return if destroyed_by_association.present?
-
UserProjectAccessChangedService.new(user_id).execute
end
# rubocop: enable CodeReuse/ServiceClass
@@ -436,6 +435,12 @@ class Member < ApplicationRecord
end
end
+ def signup_email_valid?
+ error = validate_admin_signup_restrictions(invite_email)
+
+ errors.add(:user, error) if error
+ end
+
def update_highest_role?
return unless user_id.present?
@@ -449,6 +454,13 @@ class Member < ApplicationRecord
def project_bot?
user&.project_bot?
end
+
+ def log_invitation_token_cleanup
+ return true unless Gitlab.com? && invite? && invite_accepted_at?
+
+ error = StandardError.new("Invitation token is present but invite was already accepted!")
+ Gitlab::ErrorTracking.track_exception(error, attributes.slice(%w["invite_accepted_at created_at source_type source_id user_id id"]))
+ end
end
Member.prepend_mod_with('Member')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index cf5906a4cbf..a13133c90e9 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class GroupMember < Member
+ extend ::Gitlab::Utils::Override
include FromUnion
include CreatedAtFilterable
@@ -28,8 +29,6 @@ class GroupMember < Member
attr_accessor :last_owner, :last_blocked_owner
- self.enumerate_columns_in_select_statements = true
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -51,6 +50,19 @@ class GroupMember < Member
{ group: group }
end
+ override :refresh_member_authorized_projects
+ def refresh_member_authorized_projects
+ # Here, `destroyed_by_association` will be present if the
+ # GroupMember is being destroyed due to the `dependent: :destroy`
+ # callback on Group. In this case, there is no need to refresh the
+ # authorizations, because whenever a Group is being destroyed,
+ # its projects are also destroyed, so the removal of project_authorizations
+ # will happen behind the scenes via DB foreign keys anyway.
+ return if destroyed_by_association.present?
+
+ super
+ end
+
private
def access_level_inclusion
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 5040879e177..b45c0b6a0cc 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProjectMember < Member
+ extend ::Gitlab::Utils::Override
SOURCE_TYPE = 'Project'
belongs_to :project, foreign_key: 'source_id'
@@ -19,11 +20,6 @@ class ProjectMember < Member
.where(projects: { namespace_id: groups.select(:id) })
end
- scope :without_project_bots, -> do
- left_join_users
- .merge(User.without_project_bot)
- end
-
class << self
# Add users to projects with passed access option
#
@@ -48,7 +44,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
- Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -94,6 +90,22 @@ class ProjectMember < Member
{ project: project }
end
+ override :refresh_member_authorized_projects
+ def refresh_member_authorized_projects
+ return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
+ return unless user
+
+ # rubocop:disable CodeReuse/ServiceClass
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+
+ # Until we compare the inconsistency rates of the new, specialized service and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ UserProjectAccessChangedService.new(user_id)
+ .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
+ # rubocop:enable CodeReuse/ServiceClass
+ end
+
private
def send_invite
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 7ca83d1d68c..a090ac87cc9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -329,16 +329,16 @@ class MergeRequest < ApplicationRecord
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
- scope :order_merged_at, ->(direction) do
+ scope :order_by_metric, ->(metric, direction) do
reverse_direction = { 'ASC' => 'DESC', 'DESC' => 'ASC' }
reversed_direction = reverse_direction[direction] || raise("Unknown sort direction was given: #{direction}")
order = Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'merge_request_metrics_merged_at',
- column_expression: MergeRequest::Metrics.arel_table[:merged_at],
- order_expression: Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', direction),
- reversed_order_expression: Gitlab::Database.nulls_first_order('merge_request_metrics.merged_at', reversed_direction),
+ attribute_name: "merge_request_metrics_#{metric}",
+ column_expression: MergeRequest::Metrics.arel_table[metric],
+ order_expression: Gitlab::Database.nulls_last_order("merge_request_metrics.#{metric}", direction),
+ reversed_order_expression: Gitlab::Database.nulls_first_order("merge_request_metrics.#{metric}", reversed_direction),
order_direction: direction,
nullable: :nulls_last,
distinct: false,
@@ -353,8 +353,10 @@ class MergeRequest < ApplicationRecord
order.apply_cursor_conditions(join_metrics).order(order)
end
- scope :order_merged_at_asc, -> { order_merged_at('ASC') }
- scope :order_merged_at_desc, -> { order_merged_at('DESC') }
+ scope :order_merged_at_asc, -> { order_by_metric(:merged_at, 'ASC') }
+ scope :order_merged_at_desc, -> { order_by_metric(:merged_at, 'DESC') }
+ scope :order_closed_at_asc, -> { order_by_metric(:latest_closed_at, 'ASC') }
+ scope :order_closed_at_desc, -> { order_by_metric(:latest_closed_at, 'DESC') }
scope :preload_source_project, -> { preload(:source_project) }
scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do
@@ -452,7 +454,9 @@ class MergeRequest < ApplicationRecord
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'merged_at', 'merged_at_asc' then order_merged_at_asc
+ when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'merged_at_desc' then order_merged_at_desc
+ when 'closed_at_desc' then order_closed_at_desc
else
super
end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index 0f2a7515462..09824ed4468 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -26,7 +26,7 @@ class MergeRequestContextCommit < ApplicationRecord
# create MergeRequestContextCommit by given commit sha and it's diff file record
def self.bulk_insert(rows, **args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index 8abedd26b06..b9efebe3af2 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -14,7 +14,7 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_context_commit_diff_files', *args) # rubocop:disable Gitlab/BulkInsert
end
def path
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d2ea663551d..bea75927b2c 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -395,10 +395,10 @@ class MergeRequestDiff < ApplicationRecord
if comparison
if diff_options[:paths].blank? && !without_files?
# Return the empty MergeRequestDiffBatch for an out of bound batch request
- break diffs_batch if diffs_batch.diff_file_paths.blank?
+ break diffs_batch if diffs_batch.diff_paths.blank?
diff_options.merge!(
- paths: diffs_batch.diff_file_paths,
+ paths: diffs_batch.diff_paths,
pagination_data: diffs_batch.pagination_data
)
end
@@ -515,7 +515,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
save!
end
@@ -535,7 +535,7 @@ class MergeRequestDiff < ApplicationRecord
transaction do
MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
update!(stored_externally: false)
end
@@ -595,7 +595,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_external_merge_request_diff_files(rows) if use_external_diff?
# Faster inserts
- Gitlab::Database.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('merge_request_diff_files', rows) # rubocop:disable Gitlab/BulkInsert
end
def build_external_diff_tempfile(rows)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 466d28301c0..d9a1784cdda 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -63,7 +63,7 @@ class MergeRequestDiffCommit < ApplicationRecord
)
end
- Gitlab::Database.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(self.table_name, rows) # rubocop:disable Gitlab/BulkInsert
end
def self.prepare_commits_for_bulk_insert(commits)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2168d57693e..0e2842c3c11 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -61,10 +61,38 @@ class Milestone < ApplicationRecord
end
def self.reference_pattern
+ if Feature.enabled?(:milestone_reference_pattern, default_enabled: :yaml)
+ new_reference_pattern
+ else
+ old_reference_pattern
+ end
+ end
+
+ def self.new_reference_pattern
+ # NOTE: The iid pattern only matches when all characters on the expression
+ # are digits, so it will match %2 but not %2.1 because that's probably a
+ # milestone name and we want it to be matched as such.
+ @new_reference_pattern ||= %r{
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
+ (?:
+ (?<milestone_iid>
+ \d+(?!\S\w)\b # Integer-based milestone iid, or
+ ) |
+ (?<milestone_name>
+ [^"\s\<]+\b | # String-based single-word milestone title, or
+ "[^"]+" # String-based multi-word milestone surrounded in quotes
+ )
+ )
+ }x
+ end
+
+ # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/336268
+ def self.old_reference_pattern
# NOTE: The iid pattern only matches when all characters on the expression
# are digits, so it will match %2 but not %2.1 because that's probably a
# milestone name and we want it to be matched as such.
- @reference_pattern ||= %r{
+ @old_reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5524fec5324..261639a4ec1 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -24,6 +24,7 @@ class Namespace < ApplicationRecord
NUMBER_OF_ANCESTORS_ALLOWED = 20
SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
+ URL_MAX_LENGTH = 255
cache_markdown_field :description, pipeline: :description
@@ -33,6 +34,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+ has_many :pending_builds, class_name: 'Ci::PendingBuild'
has_one :onboarding_progress
# This should _not_ be `inverse_of: :namespace`, because that would also set
@@ -58,7 +60,7 @@ class Namespace < ApplicationRecord
validates :description, length: { maximum: 255 }
validates :path,
presence: true,
- length: { maximum: 255 },
+ length: { maximum: URL_MAX_LENGTH },
namespace_path: true
# Introduce minimal path length of 2 characters.
@@ -464,10 +466,34 @@ class Namespace < ApplicationRecord
end
def refresh_access_of_projects_invited_groups
- Group
- .joins(project_group_links: :project)
- .where(projects: { namespace_id: id })
- .find_each(&:refresh_members_authorized_projects)
+ if Feature.enabled?(:specialized_worker_for_group_lock_update_auth_recalculation)
+ Project
+ .where(namespace_id: id)
+ .joins(:project_group_links)
+ .distinct
+ .find_each do |project|
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
+ end
+
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .distinct
+ .find_each do |group|
+ group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+ end
+ else
+ Group
+ .joins(project_group_links: :project)
+ .where(projects: { namespace_id: id })
+ .find_each(&:refresh_members_authorized_projects)
+ end
end
def nesting_level_allowed
@@ -503,7 +529,7 @@ class Namespace < ApplicationRecord
def write_projects_repository_config
all_projects.find_each do |project|
- project.write_repository_config
+ project.set_full_path
project.track_project_repository
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index fc890bf687c..4a39bfebda0 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -11,6 +11,9 @@ class NamespaceSetting < ApplicationRecord
validate :allow_mfa_for_group
validate :allow_resource_access_token_creation_for_group
+ before_save :set_prevent_sharing_groups_outside_hierarchy, if: -> { user_cap_enabled? }
+ after_save :disable_project_sharing!, if: -> { user_cap_enabled? }
+
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name, :delayed_project_removal,
@@ -19,10 +22,20 @@ class NamespaceSetting < ApplicationRecord
self.primary_key = :namespace_id
+ def prevent_sharing_groups_outside_hierarchy
+ return super if namespace.root?
+
+ namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
+ end
+
private
def normalize_default_branch_name
- self.default_branch_name = nil if default_branch_name.blank?
+ self.default_branch_name = if default_branch_name.blank?
+ nil
+ else
+ Sanitize.fragment(self.default_branch_name)
+ end
end
def default_branch_name_content
@@ -44,6 +57,18 @@ class NamespaceSetting < ApplicationRecord
errors.add(:resource_access_token_creation_allowed, _('is not allowed since the group is not top-level group.'))
end
end
+
+ def set_prevent_sharing_groups_outside_hierarchy
+ self.prevent_sharing_groups_outside_hierarchy = true
+ end
+
+ def disable_project_sharing!
+ namespace.update_attribute(:share_with_group_lock, true)
+ end
+
+ def user_cap_enabled?
+ new_user_signups_cap.present? && namespace.root?
+ end
end
NamespaceSetting.prepend_mod_with('NamespaceSetting')
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 3d78f384634..33e8c3e5172 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -37,6 +37,7 @@ module Namespaces
module Traversal
module Linear
extend ActiveSupport::Concern
+ include LinearScopes
UnboundedSearch = Class.new(StandardError)
@@ -44,14 +45,6 @@ module Namespaces
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? }
-
- scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
- # When filtering namespaces by the traversal_ids column to compile a
- # list of namespace IDs, it's much faster to reference the ID in
- # traversal_ids than the primary key ID column.
- # WARNING This scope must be used behind a linear query feature flag
- # such as `use_traversal_ids`.
- scope :as_ids, -> { select('traversal_ids[array_length(traversal_ids, 1)] AS id') }
end
def sync_traversal_ids?
@@ -59,7 +52,7 @@ module Namespaces
end
def use_traversal_ids?
- return false unless Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ return false unless Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
traversal_ids.present?
end
@@ -164,20 +157,14 @@ module Namespaces
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
- self.class.unscope(where: :type)
- end
-
# Search this namespace's lineage. Bound inclusively by top node.
def lineage(top: nil, bottom: nil, hierarchy_order: nil)
raise UnboundedSearch, 'Must bound search by either top or bottom' unless top || bottom
- skope = without_sti_condition
+ skope = self.class.without_sti_condition
if top
- skope = skope.traversal_ids_contains("{#{top.id}}")
+ skope = skope.where("traversal_ids @> ('{?}')", top.id)
end
if bottom
@@ -190,7 +177,13 @@ module Namespaces
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)
+ # The SELECT includes an extra depth attribute. We wrap the SQL in a
+ # standard SELECT to avoid mismatched attribute errors when trying to
+ # chain future ActiveRelation commands, and retain the ordering.
+ skope = self.class
+ .without_sti_condition
+ .from(skope, self.class.table_name)
+ .order(depth: hierarchy_order)
end
skope
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
new file mode 100644
index 00000000000..90fae8ef35d
--- /dev/null
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module LinearScopes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # When filtering namespaces by the traversal_ids column to compile a
+ # list of namespace IDs, it can be faster to reference the ID in
+ # traversal_ids than the primary key ID column.
+ def as_ids
+ return super unless use_traversal_ids?
+
+ select('namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id')
+ end
+
+ def self_and_descendants(include_self: true)
+ return super unless use_traversal_ids?
+
+ records = self_and_descendants_with_duplicates(include_self: include_self)
+
+ distinct = records.select('DISTINCT on(namespaces.id) namespaces.*')
+
+ # Produce a query of the form: SELECT * FROM namespaces;
+ #
+ # When we have queries that break this SELECT * format we can run in to errors.
+ # For example `SELECT DISTINCT on(...)` will fail when we chain a `.count` c
+ unscoped.without_sti_condition.from(distinct, :namespaces)
+ end
+
+ def self_and_descendant_ids(include_self: true)
+ return super unless use_traversal_ids?
+
+ self_and_descendants_with_duplicates(include_self: include_self)
+ .select('DISTINCT namespaces.id')
+ 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
+ unscope(where: :type)
+ end
+
+ private
+
+ def use_traversal_ids?
+ Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
+ end
+
+ def self_and_descendants_with_duplicates(include_self: true)
+ base_ids = select(:id)
+
+ records = unscoped
+ .without_sti_condition
+ .from("namespaces, (#{base_ids.to_sql}) base")
+ .where('namespaces.traversal_ids @> ARRAY[base.id]')
+
+ if include_self
+ records
+ else
+ records.where('namespaces.id <> base.id')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb
index d9e8743aa50..c1ada715d6d 100644
--- a/app/models/namespaces/traversal/recursive.rb
+++ b/app/models/namespaces/traversal/recursive.rb
@@ -4,6 +4,7 @@ module Namespaces
module Traversal
module Recursive
extend ActiveSupport::Concern
+ include RecursiveScopes
def root_ancestor
return self if parent.nil?
diff --git a/app/models/namespaces/traversal/recursive_scopes.rb b/app/models/namespaces/traversal/recursive_scopes.rb
new file mode 100644
index 00000000000..be49d5d9d55
--- /dev/null
+++ b/app/models/namespaces/traversal/recursive_scopes.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Namespaces
+ module Traversal
+ module RecursiveScopes
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def as_ids
+ select('id')
+ end
+
+ def descendant_ids
+ recursive_descendants.as_ids
+ end
+ alias_method :recursive_descendant_ids, :descendant_ids
+
+ def self_and_descendants(include_self: true)
+ base = if include_self
+ unscoped.where(id: all.as_ids)
+ else
+ unscoped.where(parent_id: all.as_ids)
+ end
+
+ Gitlab::ObjectHierarchy.new(base).base_and_descendants
+ end
+ alias_method :recursive_self_and_descendants, :self_and_descendants
+
+ def self_and_descendant_ids(include_self: true)
+ self_and_descendants(include_self: include_self).as_ids
+ end
+ alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
+ end
+ end
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2ad6df85e5f..34ffd7c91af 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -5,6 +5,8 @@
# A note of this type is never resolvable.
class Note < ApplicationRecord
extend ActiveModel::Naming
+ extend Gitlab::Utils::Override
+
include Gitlab::Utils::StrongMemoize
include Participable
include Mentionable
@@ -576,6 +578,29 @@ class Note < ApplicationRecord
review.present? || !author.can_trigger_notifications?
end
+ def post_processed_cache_key
+ cache_key_items = [cache_key, author.cache_key]
+ cache_key_items << Digest::SHA1.hexdigest(redacted_note_html) if redacted_note_html.present?
+
+ cache_key_items.join(':')
+ end
+
+ override :user_mention_class
+ def user_mention_class
+ return if noteable.blank?
+
+ noteable.user_mention_class
+ end
+
+ override :user_mention_identifier
+ def user_mention_identifier
+ return if noteable.blank?
+
+ noteable.user_mention_identifier.merge({
+ note_id: id
+ })
+ end
+
private
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 4323f89865a..2e45753c182 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -16,7 +16,7 @@ class NotificationSetting < ApplicationRecord
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
- validate :owns_notification_email, if: :notification_email_changed?
+ validate :notification_email_verified, if: :notification_email_changed?
scope :for_groups, -> { where(source_type: 'Namespace') }
@@ -110,11 +110,11 @@ class NotificationSetting < ApplicationRecord
has_attribute?(event) && !!read_attribute(event)
end
- def owns_notification_email
+ def notification_email_verified
return if user.temp_oauth_email?
return if notification_email.empty?
- errors.add(:notification_email, _("is not an email you own")) unless user.verified_emails.include?(notification_email)
+ errors.add(:notification_email, _("must be an email you have verified")) unless user.verified_emails.include?(notification_email)
end
end
diff --git a/app/models/operations/feature_flags/strategy.rb b/app/models/operations/feature_flags/strategy.rb
index c70e10c72d5..ed9400dde8f 100644
--- a/app/models/operations/feature_flags/strategy.rb
+++ b/app/models/operations/feature_flags/strategy.rb
@@ -16,7 +16,7 @@ module Operations
STRATEGY_USERWITHID => ['userIds'].freeze
}.freeze
USERID_MAX_LENGTH = 256
- STICKINESS_SETTINGS = %w[DEFAULT USERID SESSIONID RANDOM].freeze
+ STICKINESS_SETTINGS = %w[default userId sessionId random].freeze
self.table_name = 'operations_strategies'
diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb
index e20f1b8244a..2daafe0ebcf 100644
--- a/app/models/packages/debian.rb
+++ b/app/models/packages/debian.rb
@@ -6,6 +6,8 @@ module Packages
COMPONENT_REGEX = DISTRIBUTION_REGEX.freeze
ARCHITECTURE_REGEX = %r{[a-z0-9][-a-z0-9]*}.freeze
+ LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
+
def self.table_name_prefix
'packages_debian_'
end
diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb
index a1eb7120117..bb2c33594e5 100644
--- a/app/models/packages/event.rb
+++ b/app/models/packages/event.rb
@@ -4,7 +4,7 @@ class Packages::Event < ApplicationRecord
belongs_to :package, optional: true
UNIQUE_EVENTS_ALLOWED = %i[push_package delete_package pull_package pull_symbol_package push_symbol_package].freeze
- EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze
+ EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001, dependency_proxy: 1002).freeze
EVENT_PREFIX = "i_package"
@@ -23,7 +23,11 @@ class Packages::Event < ApplicationRecord
list_tags: 9,
cli_metadata: 10,
pull_symbol_package: 11,
- push_symbol_package: 12
+ push_symbol_package: 12,
+ pull_manifest: 13,
+ pull_manifest_from_cache: 14,
+ pull_blob: 15,
+ pull_blob_from_cache: 16
}
enum originator_type: { user: 0, deploy_token: 1, guest: 2 }
diff --git a/app/models/packages/npm.rb b/app/models/packages/npm.rb
new file mode 100644
index 00000000000..e49199d911c
--- /dev/null
+++ b/app/models/packages/npm.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+module Packages
+ module Npm
+ # from "@scope/package-name" return "scope" or nil
+ def self.scope_of(package_name)
+ return unless package_name
+ return unless package_name.starts_with?('@')
+ return unless package_name.include?('/')
+
+ package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index d2e4f46898c..4ea127fc222 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord
validate :valid_conan_package_recipe, if: :conan?
validate :valid_composer_global_name, if: :composer?
- validate :package_already_taken, if: :npm?
+ validate :npm_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?
@@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord
end
end
- def package_already_taken
+ def npm_package_already_taken
return unless project
+ return unless follows_npm_naming_convention?
- if project.package_already_taken?(name)
+ if project.package_already_taken?(name, version, package_type: :npm)
errors.add(:base, _('Package already exists'))
end
end
+ # https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
+ def follows_npm_naming_convention?
+ return false unless project&.root_namespace&.path
+
+ project.root_namespace.path == ::Packages::Npm.scope_of(name)
+ end
+
def unique_debian_package_name
return unless debian_publication&.distribution
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 799242a639a..8aa19397086 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -5,11 +5,14 @@ class Packages::PackageFile < ApplicationRecord
delegate :project, :project_id, to: :package
delegate :conan_file_type, to: :conan_file_metadatum
- delegate :file_type, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
+ delegate :file_type, :dsc?, :component, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian
delegate :channel, :metadata, to: :helm_file_metadatum, prefix: :helm
belongs_to :package
+ # used to move the linked file within object storage
+ attribute :new_file_path, default: nil
+
has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum'
has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo'
has_many :pipelines, through: :package_file_build_infos
@@ -33,6 +36,8 @@ class Packages::PackageFile < ApplicationRecord
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) }
scope :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) }
+
+ scope :preload_package, -> { preload(:package) }
scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) }
scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) }
scope :preload_helm_file_metadata, -> { preload(:helm_file_metadatum) }
@@ -78,6 +83,12 @@ class Packages::PackageFile < ApplicationRecord
before_save :update_size_from_file
+ # if a new_file_path is provided, we need
+ # * disable the remove_previously_stored_file callback so that carrierwave doesn't take care of the file
+ # * enable a new after_commit callback that will move the file in object storage
+ skip_callback :commit, :after, :remove_previously_stored_file, if: :execute_move_in_object_storage?
+ after_commit :move_in_object_storage, if: :execute_move_in_object_storage?
+
def download_path
Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
end
@@ -87,6 +98,17 @@ class Packages::PackageFile < ApplicationRecord
def update_size_from_file
self.size ||= file.size
end
+
+ def execute_move_in_object_storage?
+ !file.file_storage? && new_file_path?
+ end
+
+ def move_in_object_storage
+ carrierwave_file = file.file
+
+ carrierwave_file.copy_to(new_file_path)
+ carrierwave_file.delete
+ end
end
Packages::PackageFile.prepend_mod_with('Packages::PackageFile')
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 732ed0b7bb3..1778e927dd1 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -47,6 +47,10 @@ class PersonalAccessToken < ApplicationRecord
!revoked? && !expired?
end
+ def expired_but_not_enforced?
+ false
+ end
+
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
redis_key = redis_shared_state_key(user_id)
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
new file mode 100644
index 00000000000..76b299ff9d4
--- /dev/null
+++ b/app/models/postgresql/detached_partition.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Postgresql
+ class DetachedPartition < ApplicationRecord
+ scope :ready_to_drop, -> { where('drop_after < ?', Time.current) }
+ end
+end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index 77b42c34ad9..1a4d3bd5794 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -39,5 +39,55 @@ module Postgresql
false
end
end
+
+ def self.count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots;")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ def self.unused_slots_count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ def self.used_slots_count
+ connection
+ .execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 't';")
+ .first
+ .fetch('count')
+ .to_i
+ end
+
+ # array of slots and the retained_bytes
+ # https://www.skillslogic.com/blog/databases/checking-postgres-replication-lag
+ # http://bdr-project.org/docs/stable/monitoring-peers.html
+ def self.slots_retained_bytes
+ connection.execute(<<-SQL.squish).to_a
+ SELECT slot_name, database,
+ active, pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)
+ AS retained_bytes
+ FROM pg_replication_slots;
+ SQL
+ end
+
+ # returns the max number WAL space (in bytes) being used across the replication slots
+ def self.max_retained_wal
+ connection.execute(<<-SQL.squish).first.fetch('coalesce').to_i
+ SELECT COALESCE(MAX(pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)), 0)
+ FROM pg_replication_slots;
+ SQL
+ end
+
+ def self.max_replication_slots
+ connection.execute(<<-SQL.squish).first&.fetch('setting').to_i
+ SELECT setting FROM pg_settings WHERE name = 'max_replication_slots';
+ SQL
+ end
end
end
diff --git a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
index c0ed56057ae..3764e9dcb16 100644
--- a/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
+++ b/app/models/preloaders/user_max_access_level_in_projects_preloader.rb
@@ -10,9 +10,13 @@ module Preloaders
end
def execute
+ # Use reselect to override the existing select to prevent
+ # the error `subquery has too many columns`
+ # NotificationsController passes in an Array so we need to check the type
+ project_ids = @projects.is_a?(ActiveRecord::Relation) ? @projects.reselect(:id) : @projects
access_levels = @user
.project_authorizations
- .where(project_id: @projects)
+ .where(project_id: project_ids)
.group(:project_id)
.maximum(:access_level)
diff --git a/app/models/project.rb b/app/models/project.rb
index c5522737b87..81b04e1316c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -43,8 +43,13 @@ class Project < ApplicationRecord
extend Gitlab::ConfigHelper
+ ignore_columns :container_registry_enabled, remove_after: '2021-09-22', remove_with: '14.4'
+
BoardLimitExceeded = Class.new(StandardError)
+ ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4'
+ ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4'
+
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
@@ -73,7 +78,6 @@ class Project < ApplicationRecord
default_value_for :packages_enabled, true
default_value_for :archived, false
default_value_for :resolve_outdated_diff_discussions, false
- default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) do
Repository.pick_storage_shard
end
@@ -95,9 +99,6 @@ class Project < ApplicationRecord
before_save :ensure_runners_token
- # https://api.rubyonrails.org/v6.0.3.4/classes/ActiveRecord/AttributeMethods/Dirty.html#method-i-will_save_change_to_attribute-3F
- before_update :set_container_registry_access_level, if: :will_save_change_to_container_registry_enabled?
-
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
@@ -318,7 +319,6 @@ class Project < ApplicationRecord
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
- has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
has_many :job_artifacts, class_name: 'Ci::JobArtifact'
@@ -378,6 +378,7 @@ class Project < ApplicationRecord
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error'
+ has_many :error_tracking_client_keys, inverse_of: :project, class_name: 'ErrorTracking::ClientKey'
has_many :timelogs
@@ -436,7 +437,7 @@ class Project < ApplicationRecord
delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, to: :ci_cd_settings, allow_nil: true
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
- :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?,
+ :allow_merge_on_skipped_pipeline=, :has_confluence?,
to: :project_setting
delegate :active?, to: :prometheus_integration, allow_nil: true, prefix: true
@@ -538,10 +539,8 @@ class Project < ApplicationRecord
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) }
- 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_integrations, -> { joins(:integrations).merge(::Integrations::Jira.active) }
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) }
@@ -549,7 +548,9 @@ class Project < ApplicationRecord
scope :with_namespace, -> { includes(:namespace) }
scope :with_import_state, -> { includes(:import_state) }
scope :include_project_feature, -> { includes(:project_feature) }
- scope :with_integration, ->(integration) { joins(integration).eager_load(integration) }
+ scope :include_integration, -> (integration_association_name) { includes(integration_association_name) }
+ scope :with_integration, -> (integration_class) { joins(:integrations).merge(integration_class.all) }
+ scope :with_active_integration, -> (integration_class) { with_integration(integration_class).merge(integration_class.active) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
@@ -913,7 +914,13 @@ class Project < ApplicationRecord
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
- alias_method :ancestors, :ancestors_upto
+ def ancestors(hierarchy_order: nil)
+ if Feature.enabled?(:linear_project_ancestors, self, default_enabled: :yaml)
+ group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none
+ else
+ ancestors_upto(hierarchy_order: hierarchy_order)
+ end
+ end
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
@@ -1180,6 +1187,15 @@ class Project < ApplicationRecord
import_type == 'gitea'
end
+ def github_import?
+ import_type == 'github'
+ end
+
+ def github_enterprise_import?
+ github_import? &&
+ URI.parse(import_url).host != URI.parse(Octokit::Default::API_ENDPOINT).host
+ end
+
def has_remote_mirror?
remote_mirror_available? && remote_mirrors.enabled.exists?
end
@@ -1411,14 +1427,13 @@ class Project < ApplicationRecord
def find_or_initialize_integration(name)
return if disabled_integrations.include?(name)
- find_integration(integrations, name) || build_from_instance_or_template(name) || build_integration(name)
+ find_integration(integrations, name) || build_from_instance(name) || build_integration(name)
end
# rubocop: disable CodeReuse/ServiceClass
def create_labels
Label.templates.each do |label|
- # TODO: remove_on_close exception can be removed after the column is dropped from all envs
- params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type', 'remove_on_close')
+ params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
@@ -1876,11 +1891,11 @@ class Project < ApplicationRecord
.update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
end
- def write_repository_config(gl_full_path: full_path)
+ def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
- repository.raw_repository.write_config(full_path: gl_full_path)
+ repository.raw_repository.set_full_path(full_path: gl_full_path)
rescue Gitlab::Git::Repository::NoRepository => e
Gitlab::AppLogger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
@@ -1892,6 +1907,7 @@ class Project < ApplicationRecord
DetectRepositoryLanguagesWorker.perform_async(id)
ProjectCacheWorker.perform_async(self.id, [], [:repository_size])
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(id)
# The import assigns iid values on its own, e.g. by re-using GitHub ids.
# Flush existing InternalId records for this project for consistency reasons.
@@ -1904,7 +1920,7 @@ class Project < ApplicationRecord
after_create_default_branch
join_pool_repository
refresh_markdown_cache!
- write_repository_config
+ set_full_path
end
def update_project_counter_caches
@@ -2030,6 +2046,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
.append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase)
+ .append(key: 'CI_PROJECT_CLASSIFICATION_LABEL', value: external_authorization_classification_label)
.append(key: 'CI_DEFAULT_BRANCH', value: default_branch)
.append(key: 'CI_CONFIG_PATH', value: ci_config_path_or_default)
end
@@ -2557,12 +2574,15 @@ class Project < ApplicationRecord
[project&.id, root_group&.id]
end
- def package_already_taken?(package_name)
- namespace.root_ancestor.all_projects
- .joins(:packages)
- .where.not(id: id)
- .merge(Packages::Package.default_scoped.with_name(package_name))
- .exists?
+ def package_already_taken?(package_name, package_version, package_type:)
+ Packages::Package.with_name(package_name)
+ .with_version(package_version)
+ .with_package_type(package_type)
+ .for_projects(
+ root_ancestor.all_projects
+ .id_not_in(id)
+ .select(:id)
+ ).exists?
end
def default_branch_or_main
@@ -2651,40 +2671,22 @@ class Project < ApplicationRecord
private
- def set_container_registry_access_level
- # changes_to_save = { 'container_registry_enabled' => [value_before_update, value_after_update] }
- value = changes_to_save['container_registry_enabled'][1]
-
- access_level =
- if value
- ProjectFeature::ENABLED
- else
- ProjectFeature::DISABLED
- end
-
- project_feature.update!(container_registry_access_level: access_level)
- end
-
def find_integration(integrations, name)
integrations.find { _1.to_param == name }
end
- def build_from_instance_or_template(name)
+ def build_from_instance(name)
instance = find_integration(integration_instances, name)
- return Integration.build_from_integration(instance, project_id: id) if instance
- template = find_integration(integration_templates, name)
- return Integration.build_from_integration(template, project_id: id) if template
+ return unless instance
+
+ Integration.build_from_integration(instance, project_id: id)
end
def build_integration(name)
Integration.integration_name_to_model(name).new(project_id: id)
end
- def integration_templates
- @integration_templates ||= Integration.for_template
- end
-
def integration_instances
@integration_instances ||= Integration.for_instance
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index f6e889396c6..aea8abecd74 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -2,6 +2,7 @@
class ProjectFeature < ApplicationRecord
include Featurable
+ extend Gitlab::ConfigHelper
# When updating this array, make sure to update rubocop/cop/gitlab/feature_available_usage.rb as well.
FEATURES = %i[
@@ -48,12 +49,7 @@ class ProjectFeature < ApplicationRecord
end
end
- before_create :set_container_registry_access_level
-
- # Default scopes force us to unscope here since a service may need to check
- # permissions for a project in pending_delete
- # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
- belongs_to :project, -> { unscope(where: :pending_delete) }
+ belongs_to :project
validates :project, presence: true
@@ -80,6 +76,14 @@ class ProjectFeature < ApplicationRecord
end
end
+ default_value_for(:container_registry_access_level) do |feature|
+ if gitlab_config_features.container_registry
+ ENABLED
+ else
+ DISABLED
+ end
+ end
+
def public_pages?
return true unless Gitlab.config.pages.access_control
@@ -94,15 +98,6 @@ class ProjectFeature < ApplicationRecord
private
- def set_container_registry_access_level
- self.container_registry_access_level =
- if project&.read_attribute(:container_registry_enabled)
- ENABLED
- else
- DISABLED
- end
- end
-
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 24d892290a6..b2559636f32 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
+ include IgnorableColumns
+
+ ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10'
+
belongs_to :project, inverse_of: :project_setting
enum squash_option: {
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 4586aa2b4b4..4ae3bc01a01 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -42,7 +42,7 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass
project,
users,
access_level,
@@ -52,7 +52,7 @@ class ProjectTeam
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
- Members::Projects::CreatorService.new(project, # rubocop:todo CodeReuse/ServiceClass
+ Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass
user,
access_level,
current_user: current_user,
@@ -78,6 +78,10 @@ class ProjectTeam
members.where(id: member_user_ids)
end
+ def members_with_access_levels(access_levels = [])
+ fetch_members(access_levels)
+ end
+
def guests
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
diff --git a/app/models/projects/ci_feature_usage.rb b/app/models/projects/ci_feature_usage.rb
new file mode 100644
index 00000000000..a10426e50c9
--- /dev/null
+++ b/app/models/projects/ci_feature_usage.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Projects
+ class CiFeatureUsage < ApplicationRecord
+ self.table_name = 'project_ci_feature_usages'
+
+ belongs_to :project
+
+ validates :project, :feature, presence: true
+
+ enum feature: {
+ code_coverage: 1,
+ security_report: 2
+ }
+
+ def self.insert_usage(project_id:, feature:, default_branch:)
+ insert(
+ {
+ project_id: project_id,
+ feature: feature,
+ default_branch: default_branch
+ },
+ unique_by: 'index_project_ci_feature_usages_unique_columns'
+ )
+ end
+ end
+end
diff --git a/app/models/release_highlight.rb b/app/models/release_highlight.rb
index 84e0a43670b..17a9ad7db66 100644
--- a/app/models/release_highlight.rb
+++ b/app/models/release_highlight.rb
@@ -49,8 +49,12 @@ class ReleaseHighlight
end
def self.file_paths
- @file_paths ||= Rails.cache.fetch(self.cache_key('file_paths'), expires_in: CACHE_DURATION) do
- Dir.glob(FILES_PATH).sort.reverse
+ @file_paths ||= self.relative_file_paths.map { |path| path.prepend(Rails.root.to_s) }
+ end
+
+ def self.relative_file_paths
+ Rails.cache.fetch(self.cache_key('file_paths'), expires_in: CACHE_DURATION) do
+ Dir.glob(FILES_PATH).sort.reverse.map { |path| path.delete_prefix(Rails.root.to_s) }
end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index a700f104150..7f41f0907d5 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -22,14 +22,9 @@ class RemoteMirror < ApplicationRecord
validates :url, presence: true, public_url: { schemes: %w(ssh git http https), allow_blank: true, enforce_user: true }
- before_save :set_new_remote_name, if: :mirror_url_changed?
-
after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
- after_save :refresh_remote, if: :saved_change_to_mirror_url?
after_update :reset_fields, if: :saved_change_to_mirror_url?
- after_commit :remove_remote, on: :destroy
-
before_validation :store_credentials
scope :enabled, -> { where(enabled: true) }
@@ -88,10 +83,6 @@ class RemoteMirror < ApplicationRecord
end
end
- def remote_name
- super || fallback_remote_name
- end
-
def update_failed?
update_status == 'failed'
end
@@ -100,11 +91,10 @@ class RemoteMirror < ApplicationRecord
update_status == 'started'
end
- def update_repository(inmemory_remote:)
+ def update_repository
Gitlab::Git::RemoteMirror.new(
project.repository.raw,
- remote_name,
- inmemory_remote ? remote_url : nil,
+ remote_url,
**options_for_update
).update
end
@@ -227,15 +217,6 @@ class RemoteMirror < ApplicationRecord
Gitlab::UrlSanitizer.new(read_attribute(:url)).full_url
end
- def ensure_remote!
- return unless project
- return unless remote_name && remote_url
-
- # If this fails or the remote already exists, we won't know due to
- # https://gitlab.com/gitlab-org/gitaly/issues/1317
- project.repository.add_remote(remote_name, remote_url)
- end
-
def after_sent_notification
update_column(:error_notification_sent, true)
end
@@ -280,12 +261,6 @@ class RemoteMirror < ApplicationRecord
super
end
- def fallback_remote_name
- return unless id
-
- "remote_mirror_#{id}"
- end
-
def recently_scheduled?
return false unless self.last_update_started_at
@@ -308,29 +283,6 @@ class RemoteMirror < ApplicationRecord
project.update(remote_mirror_available_overridden: enabled)
end
- def set_new_remote_name
- self.remote_name = "remote_mirror_#{SecureRandom.hex}"
- end
-
- def refresh_remote
- return unless project
-
- # Before adding a new remote we have to delete the data from
- # the previous remote name
- prev_remote_name = remote_name_before_last_save || fallback_remote_name
- run_after_commit do
- project.repository.async_remove_remote(prev_remote_name)
- end
-
- project.repository.add_remote(remote_name, remote_url)
- end
-
- def remove_remote
- return unless project # could be pending to delete so don't need to touch the git repository
-
- project.repository.async_remove_remote(remote_name)
- end
-
def mirror_url_changed?
url_changed? || attribute_changed?(:credentials)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a77aaf02e06..0164d6fed93 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -168,8 +168,8 @@ class Repository
end
# Returns a list of commits that are not present in any reference
- def new_commits(newrev)
- commits = raw.new_commits(newrev)
+ def new_commits(newrev, allow_quarantine: false)
+ commits = raw.new_commits(newrev, allow_quarantine: allow_quarantine)
::Commit.decorate(commits, container)
end
@@ -502,8 +502,8 @@ class Repository
end
end
- def blob_at(sha, path)
- Blob.decorate(raw_repository.blob_at(sha, path), container)
+ def blob_at(sha, path, limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ Blob.decorate(raw_repository.blob_at(sha, path, limit: limit), container)
rescue Gitlab::Git::Repository::NoRepository
nil
end
@@ -656,7 +656,7 @@ class Repository
end
end
- def tree(sha = :head, path = nil, recursive: false)
+ def tree(sha = :head, path = nil, recursive: false, pagination_params: nil)
if sha == :head
return unless head_commit
@@ -667,7 +667,7 @@ class Repository
end
end
- Tree.new(self, sha, path, recursive: recursive)
+ Tree.new(self, sha, path, recursive: recursive, pagination_params: pagination_params)
end
def blob_at_branch(branch_name, path)
@@ -938,33 +938,8 @@ class Repository
end
end
- def fetch_as_mirror(url, forced: false, refmap: :all_refs, remote_name: nil, prune: true)
- return fetch_remote(remote_name, url: url, refmap: refmap, forced: forced, prune: prune) if Feature.enabled?(:fetch_remote_params, project, default_enabled: :yaml)
-
- unless remote_name
- remote_name = "tmp-#{SecureRandom.hex}"
- tmp_remote_name = true
- end
-
- add_remote(remote_name, url, mirror_refmap: refmap)
- fetch_remote(remote_name, forced: forced, prune: prune)
- ensure
- async_remove_remote(remote_name) if tmp_remote_name
- end
-
- def async_remove_remote(remote_name)
- return unless remote_name
- return unless project
-
- job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
-
- if job_id
- Gitlab::AppLogger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
- else
- Gitlab::AppLogger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
- end
-
- job_id
+ def fetch_as_mirror(url, forced: false, refmap: :all_refs, prune: true, http_authorization_header: "")
+ fetch_remote(url, refmap: refmap, forced: forced, prune: prune, http_authorization_header: http_authorization_header)
end
def fetch_source_branch!(source_repository, source_branch, local_ref)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 68957dd6b22..dd76f2c3c84 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -246,7 +246,7 @@ class Snippet < ApplicationRecord
notes.includes(:author)
end
- def check_for_spam?
+ def check_for_spam?(user:)
visibility_level_changed?(to: Snippet::PUBLIC) ||
(public? && (title_changed? || content_changed?))
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 8aeeae1330c..8c3b85ac4c3 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -20,7 +20,6 @@ module Terraform
foreign_key: :terraform_state_id,
inverse_of: :terraform_state
- scope :versioning_not_enabled, -> { where(versioning_enabled: false) }
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index 3f0e827cf61..7c394736560 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -19,6 +19,14 @@ class Timelog < ApplicationRecord
joins(:project).where(projects: { namespace: group.self_and_descendants })
end
+ scope :in_project, -> (project) do
+ where(project: project)
+ end
+
+ scope :for_user, -> (user) do
+ where(user: user)
+ end
+
scope :at_or_after, -> (start_time) do
where('spent_at >= ?', start_time)
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index cd385872171..fd416ebdedc 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -4,9 +4,9 @@ class Tree
include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
- attr_accessor :repository, :sha, :path, :entries
+ attr_accessor :repository, :sha, :path, :entries, :cursor
- def initialize(repository, sha, path = '/', recursive: false)
+ def initialize(repository, sha, path = '/', recursive: false, pagination_params: nil)
path = '/' if path.blank?
@repository = repository
@@ -14,7 +14,7 @@ class Tree
@path = path
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
+ @entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, pagination_params)
end
def readme_path
diff --git a/app/models/user.rb b/app/models/user.rb
index 80b8c9173d1..cb0f15c04cb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,6 +26,7 @@ class User < ApplicationRecord
include UpdateHighestRole
include HasUserType
include Gitlab::Auth::Otp::Fortinet
+ include RestrictedSignup
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -205,11 +206,14 @@ class User < ApplicationRecord
has_one :user_canonical_email
has_one :credit_card_validation, class_name: '::Users::CreditCardValidation'
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
+ has_one :banned_user, class_name: '::Users::BannedUser'
has_many :reviews, foreign_key: :author_id, inverse_of: :author
has_many :in_product_marketing_emails, class_name: '::Users::InProductMarketingEmail'
+ has_many :timelogs
+
#
# Validations
#
@@ -220,7 +224,7 @@ class User < ApplicationRecord
validates :email, confirmation: true
validates :notification_email, presence: true
validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email }
- validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true
+ validates :public_email, uniqueness: true, devise_email: true, allow_blank: true
validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email }
validates :projects_limit,
presence: true,
@@ -231,11 +235,10 @@ class User < ApplicationRecord
validate :namespace_move_dir_allowed, if: :username_changed?
validate :unique_email, if: :email_changed?
- validate :owns_notification_email, if: :notification_email_changed?
- validate :owns_public_email, if: :public_email_changed?
- validate :owns_commit_email, if: :commit_email_changed?
- validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
- validate :check_email_restrictions, on: :create, if: ->(user) { !user.created_by_id }
+ validate :notification_email_verified, if: :notification_email_changed?
+ validate :public_email_verified, if: :public_email_changed?
+ validate :commit_email_verified, if: :commit_email_changed?
+ validate :signup_email_valid?, on: :create, if: ->(user) { !user.created_by_id }
validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
@@ -245,7 +248,6 @@ class User < ApplicationRecord
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
before_validation :sanitize_attrs
- before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
before_validation :set_commit_email, if: :commit_email_changed?
before_save :default_private_profile_to_false
@@ -270,11 +272,6 @@ class User < ApplicationRecord
update_emails_with_primary_email(previous_confirmed_at, previous_email)
update_invalid_gpg_signatures
-
- if previous_email == notification_email
- self.notification_email = email
- save
- end
end
end
@@ -315,6 +312,7 @@ class User < ApplicationRecord
delegate :bio, :bio=, :bio_html, to: :user_detail, allow_nil: true
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
+ delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -326,7 +324,6 @@ class User < ApplicationRecord
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
- transition banned: :blocked
end
event :ldap_block do
@@ -380,6 +377,14 @@ class User < ApplicationRecord
NotificationService.new.user_deactivated(user.name, user.notification_email)
end
# rubocop: enable CodeReuse/ServiceClass
+
+ after_transition active: :banned do |user|
+ user.create_banned_user
+ end
+
+ after_transition banned: :active do |user|
+ user.banned_user&.destroy
+ end
end
# Scopes
@@ -917,22 +922,22 @@ class User < ApplicationRecord
end
end
- def owns_notification_email
- return if new_record? || temp_oauth_email?
+ def notification_email_verified
+ return if read_attribute(:notification_email).blank? || temp_oauth_email?
- errors.add(:notification_email, _("is not an email you own")) unless verified_emails.include?(notification_email)
+ errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
end
- def owns_public_email
+ def public_email_verified
return if public_email.blank?
- errors.add(:public_email, _("is not an email you own")) unless verified_emails.include?(public_email)
+ errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
end
- def owns_commit_email
+ def commit_email_verified
return if read_attribute(:commit_email).blank?
- errors.add(:commit_email, _("is not an email you own")) unless verified_emails.include?(commit_email)
+ errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
end
# Define commit_email-related attribute methods explicitly instead of relying
@@ -959,6 +964,11 @@ class User < ApplicationRecord
has_attribute?(:commit_email) && super
end
+ def notification_email
+ # The notification email is the same as the primary email if undefined
+ super.presence || self.email
+ end
+
def private_commit_email
Gitlab::PrivateCommitEmail.for_user(self)
end
@@ -1005,6 +1015,8 @@ class User < ApplicationRecord
# Returns a relation of groups the user has access to, including their parent
# and child groups (recursively).
def all_expanded_groups
+ return groups if groups.empty?
+
Gitlab::ObjectHierarchy.new(groups).all_objects
end
@@ -1576,10 +1588,11 @@ class User < ApplicationRecord
.order('routes.path')
end
- def namespaces
- namespace_ids = groups.pluck(:id)
- namespace_ids.push(namespace.id)
- Namespace.where(id: namespace_ids)
+ def namespaces(owned_only: false)
+ user_groups = owned_only ? owned_groups : groups
+ personal_namespace = Namespace.where(id: namespace.id)
+
+ Namespace.from_union([user_groups, personal_namespace])
end
def oauth_authorized_tokens
@@ -2008,8 +2021,8 @@ class User < ApplicationRecord
def authorized_groups_without_shared_membership
Group.from_union([
- groups,
- authorized_projects.joins(:namespace).select('namespaces.*')
+ groups.select(Namespace.arel_table[Arel.star]),
+ authorized_projects.joins(:namespace).select(Namespace.arel_table[Arel.star])
])
end
@@ -2058,51 +2071,10 @@ class User < ApplicationRecord
end
end
- def signup_domain_valid?
- valid = true
- error = nil
-
- if Gitlab::CurrentSettings.domain_denylist_enabled?
- blocked_domains = Gitlab::CurrentSettings.domain_denylist
- if domain_matches?(blocked_domains, email)
- error = 'is not from an allowed domain.'
- valid = false
- end
- end
-
- allowed_domains = Gitlab::CurrentSettings.domain_allowlist
- unless allowed_domains.blank?
- if domain_matches?(allowed_domains, email)
- valid = true
- else
- error = "domain is not authorized for sign-up"
- valid = false
- end
- end
-
- errors.add(:email, error) unless valid
-
- valid
- end
-
- def domain_matches?(email_domains, email)
- signup_domain = Mail::Address.new(email).domain
- email_domains.any? do |domain|
- escaped = Regexp.escape(domain).gsub('\*', '.*?')
- regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
- signup_domain =~ regexp
- end
- end
+ def signup_email_valid?
+ error = validate_admin_signup_restrictions(email)
- def check_email_restrictions
- return unless Gitlab::CurrentSettings.email_restrictions_enabled?
-
- restrictions = Gitlab::CurrentSettings.email_restrictions
- return if restrictions.blank?
-
- if Gitlab::UntrustedRegexp.new(restrictions).match?(email)
- errors.add(:email, _('is not allowed. Try again with a different email address, or contact your GitLab admin.'))
- end
+ errors.add(:email, error) if error
end
def check_username_format
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 854992dcd1e..1172b2ee5e8 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -16,7 +16,6 @@ class UserCallout < ApplicationRecord
tabs_position_highlight: 10,
threat_monitoring_info: 11, # EE-only
account_recovery_regular_check: 12, # EE-only
- service_templates_deprecated_callout: 14,
web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
@@ -35,7 +34,9 @@ class UserCallout < ApplicationRecord
cloud_licensing_subscription_activation_banner: 33, # EE-only
trial_status_reminder_d14: 34, # EE-only
trial_status_reminder_d3: 35, # EE-only
- security_configuration_devops_alert: 36 # EE-only
+ security_configuration_devops_alert: 36, # EE-only
+ profile_personal_access_token_expiry: 37, # EE-only
+ terraform_notification_dismissed: 38
}
validates :user, presence: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 47537e5885f..b3cca1e0cc0 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -7,6 +7,7 @@ class UserDetail < ApplicationRecord
belongs_to :user
validates :pronouns, length: { maximum: 50 }
+ validates :pronunciation, length: { maximum: 255 }
validates :job_title, length: { maximum: 200 }
validates :bio, length: { maximum: 255 }, allow_blank: true
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 4c8cc5fc83a..1c7515894fe 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -24,16 +24,8 @@ class UserInteractedProject < ApplicationRecord
}
cached_exists?(**attributes) do
- transaction(requires_new: true) do
- where(attributes).select(1).first || create!(attributes)
- true # not caching the whole record here for now
- rescue ActiveRecord::RecordNotUnique
- # Note, above queries are not atomic and prone
- # to race conditions (similar like #find_or_create!).
- # In the case where we hit this, the record we want
- # already exists - shortcut and return.
- true
- end
+ where(attributes).exists? || UserInteractedProject.insert_all([attributes], unique_by: %w(project_id user_id))
+ true
end
end
diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb
new file mode 100644
index 00000000000..c52b6d4b728
--- /dev/null
+++ b/app/models/users/banned_user.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Users
+ class BannedUser < ApplicationRecord
+ self.primary_key = :user_id
+
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :user_id, uniqueness: { message: _("banned user already exists") }
+ end
+end
diff --git a/app/models/users/in_product_marketing_email.rb b/app/models/users/in_product_marketing_email.rb
index 3e5e7b259d8..8fe52ac7ecc 100644
--- a/app/models/users/in_product_marketing_email.rb
+++ b/app/models/users/in_product_marketing_email.rb
@@ -19,7 +19,10 @@ module Users
verify: 1,
trial: 2,
team: 3,
- experience: 4
+ experience: 4,
+ team_short: 5,
+ trial_short: 6,
+ admin_verify: 7
}, _suffix: true
scope :without_track_and_series, -> (track, series) do
diff --git a/app/models/work_item/type.rb b/app/models/work_item/type.rb
new file mode 100644
index 00000000000..16cb7a8be45
--- /dev/null
+++ b/app/models/work_item/type.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# Note: initial thinking behind `icon_name` is for it to do triple duty:
+# 1. one of our svg icon names, such as `external-link` or a new one `bug`
+# 2. if it's an absolute url, then url to a user uploaded icon/image
+# 3. an emoji, with the format of `:smile:`
+class WorkItem::Type < ApplicationRecord
+ self.table_name = 'work_item_types'
+
+ include CacheMarkdownField
+
+ cache_markdown_field :description, pipeline: :single_line
+
+ enum base_type: {
+ issue: 0,
+ incident: 1,
+ test_case: 2, ## EE-only
+ requirement: 3 ## EE-only
+ }
+
+ belongs_to :namespace, optional: true
+ has_many :work_items, class_name: 'Issue', foreign_key: :work_item_type_id, inverse_of: :work_item_type
+
+ before_validation :strip_whitespace
+
+ # TODO: review validation rules
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336919
+ validates :name, presence: true
+ validates :name, uniqueness: { case_sensitive: false, scope: [:namespace_id] }
+ validates :name, length: { maximum: 255 }
+ validates :icon_name, length: { maximum: 255 }
+
+ private
+
+ def strip_whitespace
+ name&.strip!
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 0b0edc7c452..1d0aa54c1c0 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -50,6 +50,14 @@ class GroupPolicy < BasePolicy
@subject.dependency_proxy_feature_available?
end
+ condition(:dependency_proxy_access_allowed) do
+ if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
+ access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
+ else
+ can?(:read_group)
+ end
+ end
+
desc "Deploy token with read_package_registry scope"
condition(:read_package_registry_deploy_token) do
@user.is_a?(DeployToken) && @user.groups.include?(@subject) && @user.read_package_registry
@@ -117,6 +125,7 @@ class GroupPolicy < BasePolicy
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
enable :create_custom_emoji
+ enable :create_package
enable :create_package_settings
end
@@ -134,6 +143,7 @@ class GroupPolicy < BasePolicy
end
rule { maintainer }.policy do
+ enable :destroy_package
enable :create_projects
enable :admin_pipeline
enable :admin_build
@@ -210,7 +220,7 @@ class GroupPolicy < BasePolicy
enable :read_group
end
- rule { can?(:read_group) & dependency_proxy_available }
+ rule { dependency_proxy_access_allowed & dependency_proxy_available }
.enable :read_dependency_proxy
rule { developer & dependency_proxy_available }
@@ -230,14 +240,14 @@ class GroupPolicy < BasePolicy
enable :read_label
end
- def access_level
+ def access_level(for_any_session: false)
return GroupMember::NO_ACCESS if @user.nil?
return GroupMember::NO_ACCESS unless user_is_user?
- @access_level ||= lookup_access_level!
+ @access_level ||= lookup_access_level!(for_any_session: for_any_session)
end
- def lookup_access_level!
+ def lookup_access_level!(for_any_session: false)
@subject.max_member_access_for_user(@user)
end
@@ -258,6 +268,10 @@ class GroupPolicy < BasePolicy
def resource_access_token_creation_allowed?
resource_access_token_feature_available? && group.root_ancestor.namespace_settings.resource_access_token_creation_allowed?
end
+
+ def valid_dependency_proxy_deploy_token
+ @user.is_a?(DeployToken) && @user&.valid_for_dependency_proxy? && @user&.has_access_to_group?(@subject)
+ end
end
GroupPolicy.prepend_mod_with('GroupPolicy')
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 053243e2296..74bed6b6c4e 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -15,6 +15,9 @@ class IssuePolicy < IssuablePolicy
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
+ desc "Issue is hidden"
+ condition(:hidden, scope: :subject) { @subject.hidden? }
+
desc "Issue is persisted"
condition(:persisted, scope: :subject) { @subject.persisted? }
@@ -23,6 +26,10 @@ class IssuePolicy < IssuablePolicy
prevent :read_issue_iid
end
+ rule { hidden & ~admin }.policy do
+ prevent :read_issue
+ end
+
rule { ~can?(:read_issue) }.prevent :create_note
rule { locked }.policy do
diff --git a/app/policies/packages/dependency_link_policy.rb b/app/policies/packages/dependency_link_policy.rb
new file mode 100644
index 00000000000..c4425108062
--- /dev/null
+++ b/app/policies/packages/dependency_link_policy.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+module Packages
+ class DependencyLinkPolicy < BasePolicy
+ delegate { @subject.package }
+ end
+end
diff --git a/app/policies/packages/nuget/dependency_link_metadatum_policy.rb b/app/policies/packages/nuget/dependency_link_metadatum_policy.rb
new file mode 100644
index 00000000000..18bcc1f6585
--- /dev/null
+++ b/app/policies/packages/nuget/dependency_link_metadatum_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+module Packages
+ module Nuget
+ class DependencyLinkMetadatumPolicy < BasePolicy
+ delegate { @subject.dependency_link.package }
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 6f5bbf8c021..54b11ea6041 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -159,10 +159,6 @@ class ProjectPolicy < BasePolicy
::Feature.enabled?(:build_service_proxy, @subject)
end
- condition(:respect_protected_tag_for_release_permissions) do
- ::Feature.enabled?(:evalute_protected_tag_for_release_permissions, @subject, default_enabled: :yaml)
- end
-
condition(:user_defined_variables_allowed) do
!@subject.restrict_user_defined_variables?
end
@@ -341,7 +337,7 @@ class ProjectPolicy < BasePolicy
enable :read_metrics_user_starred_dashboard
end
- rule { packages_disabled | repository_disabled }.policy do
+ rule { packages_disabled }.policy do
prevent(*create_read_update_admin_destroy(:package))
end
@@ -375,6 +371,7 @@ class ProjectPolicy < BasePolicy
enable :update_deployment
enable :create_release
enable :update_release
+ enable :destroy_release
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
@@ -538,7 +535,7 @@ class ProjectPolicy < BasePolicy
enable :read_project_for_iids
end
- rule { ~project_allowed_for_job_token }.prevent_all
+ rule { ~public_project & ~internal_access & ~project_allowed_for_job_token }.prevent_all
rule { can?(:public_access) }.policy do
enable :read_package
@@ -660,10 +657,6 @@ class ProjectPolicy < BasePolicy
rule { build_service_proxy_enabled }.enable :build_service_proxy_enabled
- rule { respect_protected_tag_for_release_permissions & can?(:developer_access) }.policy do
- enable :destroy_release
- end
-
rule { can?(:download_code) }.policy do
enable :read_repository_graphs
end
diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb
index bff80d83bef..077e4764b34 100644
--- a/app/policies/release_policy.rb
+++ b/app/policies/release_policy.rb
@@ -9,11 +9,7 @@ class ReleasePolicy < BasePolicy
!access.can_create_tag?(@subject.tag)
end
- condition(:respect_protected_tag) do
- ::Feature.enabled?(:evalute_protected_tag_for_release_permissions, @subject.project, default_enabled: :yaml)
- end
-
- rule { respect_protected_tag & protected_tag }.policy do
+ rule { protected_tag }.policy do
prevent :create_release
prevent :update_release
prevent :destroy_release
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 52f4a4e71a1..0baee614568 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -58,11 +58,7 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def all_dependencies
dependencies = super
-
- if Feature.enabled?(:preload_associations_jobs_request_api_endpoint, project, default_enabled: :yaml)
- ActiveRecord::Associations::Preloader.new.preload(dependencies, :job_artifacts_archive)
- end
-
+ ActiveRecord::Associations::Preloader.new.preload(dependencies, :job_artifacts_archive)
dependencies
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/presenters/ci/runner_presenter.rb b/app/presenters/ci/runner_presenter.rb
new file mode 100644
index 00000000000..273328afc53
--- /dev/null
+++ b/app/presenters/ci/runner_presenter.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Ci
+ class RunnerPresenter < Gitlab::View::Presenter::Delegated
+ presents :runner
+
+ def locked?
+ read_attribute(:locked) && project_type?
+ end
+ alias_method :locked, :locked?
+ end
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index a316793dae9..03e26b92922 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -48,14 +48,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
raise NotImplementedError
end
- def install_applications_cluster_path(cluster, application)
- raise NotImplementedError
- end
-
- def update_applications_cluster_path(cluster, application)
- raise NotImplementedError
- end
-
def clear_cluster_cache_path(cluster)
raise NotImplementedError
end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 26c78384144..1f2445b04a1 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -66,17 +66,14 @@ module Gitlab
link_to project_blame_path(project, tree_join(previous_commit_id, path)),
title: _('View blame prior to this change'),
aria: { label: _('View blame prior to this change') },
+ class: 'version-link',
data: { toggle: 'tooltip', placement: 'right', container: 'body' } do
- versions_sprite_icon
+ '&nbsp;'.html_safe
end
end
def project_duration
@project_duration ||= age_map_duration(groups, project)
end
-
- def versions_sprite_icon
- @versions_sprite_icon ||= sprite_icon('doc-versions', css_class: 'doc-versions align-text-bottom')
- end
end
end
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index adbe20517be..34e7084ab02 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -9,16 +9,6 @@ class GroupClusterablePresenter < ClusterablePresenter
cluster_status_group_cluster_path(clusterable, cluster, params)
end
- override :install_applications_cluster_path
- def install_applications_cluster_path(cluster, application)
- install_applications_group_cluster_path(clusterable, cluster, application)
- end
-
- override :update_applications_cluster_path
- def update_applications_cluster_path(cluster, application)
- update_applications_group_cluster_path(clusterable, cluster, application)
- end
-
override :clear_cluster_cache_path
def clear_cluster_cache_path(cluster)
clear_cache_group_cluster_path(clusterable, cluster)
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 84b3328b37f..56d91f90b2e 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -27,16 +27,6 @@ class InstanceClusterablePresenter < ClusterablePresenter
cluster_status_admin_cluster_path(cluster, params)
end
- override :install_applications_cluster_path
- def install_applications_cluster_path(cluster, application)
- install_applications_admin_cluster_path(cluster, application)
- end
-
- override :update_applications_cluster_path
- def update_applications_cluster_path(cluster, application)
- update_applications_admin_cluster_path(cluster, application)
- end
-
override :clear_cluster_cache_path
def clear_cluster_cache_path(cluster)
clear_cache_admin_cluster_path(cluster)
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 6bf5e0bd1b0..fc8a290f5f7 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -139,7 +139,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def mentioned_issues_links
- mentioned_issues = issues_mentioned_but_not_closing(current_user)
markdown(
issues_sentence(project, mentioned_issues),
pipeline: :gfm,
@@ -239,6 +238,18 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
APPROVALS_WIDGET_BASE_TYPE
end
+ def closing_issues
+ strong_memoize(:closing_issues) do
+ visible_closing_issues_for(current_user)
+ end
+ end
+
+ def mentioned_issues
+ strong_memoize(:mentioned_issues) do
+ issues_mentioned_but_not_closing(current_user)
+ end
+ end
+
private
def cached_can_be_reverted?
@@ -253,10 +264,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
# rubocop: enable CodeReuse/ServiceClass
end
- def closing_issues
- @closing_issues ||= visible_closing_issues_for(current_user)
- end
-
def pipeline
@pipeline ||= actual_head_pipeline
end
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 1c5f11ffe59..920304e743e 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -9,16 +9,6 @@ class ProjectClusterablePresenter < ClusterablePresenter
cluster_status_project_cluster_path(clusterable, cluster, params)
end
- override :install_applications_cluster_path
- def install_applications_cluster_path(cluster, application)
- install_applications_project_cluster_path(clusterable, cluster, application)
- end
-
- override :update_applications_cluster_path
- def update_applications_cluster_path(cluster, application)
- update_applications_project_cluster_path(clusterable, cluster, application)
- end
-
override :clear_cluster_cache_path
def clear_cluster_cache_path(cluster)
clear_cache_project_cluster_path(clusterable, cluster)
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 0d038d19af3..80a8ee5cb3c 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -397,16 +397,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def topics_to_show
- project.topic_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.topic_list - topics_to_show
+ project_topic_list - topics_to_show
end
def count_of_extra_topics_not_shown
- if project.topic_list.count > MAX_TOPICS_TO_SHOW
- project.topic_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
@@ -486,6 +486,12 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
**additional_params
)
end
+
+ def project_topic_list
+ strong_memoize(:project_topic_list) do
+ project.topics.map(&:name)
+ end
+ end
end
ProjectPresenter.prepend_mod_with('ProjectPresenter')
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 6fbd14f523d..4615f471639 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -133,7 +133,7 @@ class BuildDetailsEntity < JobEntity
def callout_message
return super unless build.failure_reason.to_sym == :missing_dependency_failure
- docs_url = "https://docs.gitlab.com/ee/ci/yaml/README.html#dependencies"
+ docs_url = "https://docs.gitlab.com/ee/ci/yaml/index.html#dependencies"
[
failure_message,
diff --git a/app/serializers/ci/pipeline_entity.rb b/app/serializers/ci/pipeline_entity.rb
index fa0e904fbde..a5af543f49f 100644
--- a/app/serializers/ci/pipeline_entity.rb
+++ b/app/serializers/ci/pipeline_entity.rb
@@ -7,6 +7,7 @@ class Ci::PipelineEntity < Grape::Entity
delegate :name, :failure_reason, to: :presented_pipeline
expose :id
+ expose :iid
expose :user, using: UserEntity
expose :active?, as: :active
diff --git a/app/serializers/concerns/diff_file_conflict_type.rb b/app/serializers/concerns/diff_file_conflict_type.rb
new file mode 100644
index 00000000000..29e30245dce
--- /dev/null
+++ b/app/serializers/concerns/diff_file_conflict_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module DiffFileConflictType
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ included do
+ expose :conflict_type do |diff_file, options|
+ conflict_file = conflict_file(options, diff_file)
+
+ next unless conflict_file
+
+ conflict_file.conflict_type(diff_file)
+ end
+ end
+
+ private
+
+ def conflict_file(options, diff_file)
+ strong_memoize(:conflict_file) do
+ options[:conflicts] && options[:conflicts][diff_file.new_path]
+ end
+ end
+end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index b2a544e1125..7eca56b2f48 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class DiffFileEntity < DiffFileBaseEntity
+ include DiffFileConflictType
include CommitsHelper
include IconsHelper
include Gitlab::Utils::StrongMemoize
@@ -88,10 +89,4 @@ class DiffFileEntity < DiffFileBaseEntity
# If nothing is present, inline will be the default.
options.fetch(:diff_view, :inline).to_sym
end
-
- def conflict_file(options, diff_file)
- strong_memoize(:conflict_file) do
- options[:conflicts] && options[:conflicts][diff_file.new_path]
- end
- end
end
diff --git a/app/serializers/diff_file_metadata_entity.rb b/app/serializers/diff_file_metadata_entity.rb
index 70a5b266be1..772b549314e 100644
--- a/app/serializers/diff_file_metadata_entity.rb
+++ b/app/serializers/diff_file_metadata_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class DiffFileMetadataEntity < Grape::Entity
+ include DiffFileConflictType
+
expose :added_lines
expose :removed_lines
expose :new_path
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 64f7f8bb5eb..e0565a1e506 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -71,7 +71,12 @@ class DiffsEntity < Grape::Entity
submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
DiffFileEntity.represent(diffs.diff_files,
- options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs), conflicts: conflicts))
+ options.merge(
+ submodule_links: submodule_links,
+ code_navigation_path: code_navigation_path(diffs),
+ conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts])
+ )
+ )
end
expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs|
diff --git a/app/serializers/diffs_metadata_entity.rb b/app/serializers/diffs_metadata_entity.rb
index 681e629244f..8c226130f6e 100644
--- a/app/serializers/diffs_metadata_entity.rb
+++ b/app/serializers/diffs_metadata_entity.rb
@@ -2,8 +2,13 @@
class DiffsMetadataEntity < DiffsEntity
unexpose :diff_files
- expose :diff_files, using: DiffFileMetadataEntity do |diffs, _|
- diffs.raw_diff_files(sorted: true)
+ expose :diff_files do |diffs, options|
+ DiffFileMetadataEntity.represent(
+ diffs.raw_diff_files(sorted: true),
+ options.merge(
+ conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts])
+ )
+ )
end
expose :conflict_resolution_path do |_, options|
diff --git a/app/serializers/group_link/project_group_link_entity.rb b/app/serializers/group_link/project_group_link_entity.rb
index 2ff275fff01..bcdafd8d685 100644
--- a/app/serializers/group_link/project_group_link_entity.rb
+++ b/app/serializers/group_link/project_group_link_entity.rb
@@ -3,14 +3,13 @@
module GroupLink
class ProjectGroupLinkEntity < GroupLink::GroupLinkEntity
include RequestAwareEntity
- include Projects::ProjectMembersHelper
expose :can_update do |group_link|
- can_manage_project_members?(group_link.project)
+ can?(current_user, :admin_project_member, group_link.project)
end
expose :can_remove do |group_link|
- can_manage_project_members?(group_link.project)
+ can?(current_user, :admin_project_member, group_link.project)
end
private
diff --git a/app/serializers/integrations/project_entity.rb b/app/serializers/integrations/project_entity.rb
new file mode 100644
index 00000000000..ee28c7c19c1
--- /dev/null
+++ b/app/serializers/integrations/project_entity.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ProjectEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :avatar_url
+ expose :full_name
+ expose :name
+
+ expose :full_path do |project|
+ project_path(project)
+ end
+ end
+end
diff --git a/app/serializers/integrations/project_serializer.rb b/app/serializers/integrations/project_serializer.rb
new file mode 100644
index 00000000000..b7cd266fcbf
--- /dev/null
+++ b/app/serializers/integrations/project_serializer.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Integrations
+ class ProjectSerializer < BaseSerializer
+ include WithPagination
+
+ entity Integrations::ProjectEntity
+ end
+end
diff --git a/app/serializers/jira_connect/app_data_serializer.rb b/app/serializers/jira_connect/app_data_serializer.rb
new file mode 100644
index 00000000000..994ff19f96e
--- /dev/null
+++ b/app/serializers/jira_connect/app_data_serializer.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class JiraConnect::AppDataSerializer
+ include Gitlab::Routing
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ def initialize(subscriptions, signed_in)
+ @subscriptions = subscriptions
+ @signed_in = signed_in
+ end
+
+ def as_json
+ skip_groups = @subscriptions.map(&:namespace_id)
+
+ {
+ groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }),
+ subscriptions: JiraConnect::SubscriptionEntity.represent(@subscriptions).as_json,
+ subscriptions_path: jira_connect_subscriptions_path,
+ login_path: signed_in? ? nil : jira_connect_users_path
+ }
+ end
+
+ private
+
+ def signed_in?
+ !!@signed_in
+ end
+end
diff --git a/app/serializers/jira_connect/group_entity.rb b/app/serializers/jira_connect/group_entity.rb
new file mode 100644
index 00000000000..e5a552cb9d8
--- /dev/null
+++ b/app/serializers/jira_connect/group_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class JiraConnect::GroupEntity < Grape::Entity
+ expose :name
+ expose :avatar_url
+ expose :full_name
+ expose :description
+end
diff --git a/app/serializers/jira_connect/subscription_entity.rb b/app/serializers/jira_connect/subscription_entity.rb
new file mode 100644
index 00000000000..259cd5b122f
--- /dev/null
+++ b/app/serializers/jira_connect/subscription_entity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class JiraConnect::SubscriptionEntity < Grape::Entity
+ include Gitlab::Routing
+
+ expose :created_at
+ expose :unlink_path do |subscription|
+ jira_connect_subscription_path(subscription)
+ end
+ expose :namespace, with: JiraConnect::GroupEntity, as: :group
+end
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index 6ac43e02f3c..7fba52cbe17 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -50,11 +50,7 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
MergeRequests::PipelineEntity.represent(merge_request.actual_head_pipeline, options)
end
- expose :merge_pipeline, if: ->(mr, _) {
- Feature.enabled?(:merge_request_cached_merge_pipeline_serializer, mr.project, default_enabled: :yaml) &&
- mr.merged? &&
- can?(request.current_user, :read_pipeline, mr.target_project)
- } do |merge_request, options|
+ expose :merge_pipeline, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project) } do |merge_request, options|
MergeRequests::PipelineEntity.represent(merge_request.merge_pipeline, options)
end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 3ce67d92af1..074bd2d18d7 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -19,14 +19,6 @@ class MergeRequestPollWidgetEntity < Grape::Entity
# User entities
expose :merge_user, using: UserEntity
- expose :merge_pipeline, if: ->(mr, _) {
- Feature.disabled?(:merge_request_cached_merge_pipeline_serializer, mr.project, default_enabled: :yaml) &&
- mr.merged? &&
- can?(request.current_user, :read_pipeline, mr.target_project)
- } do |merge_request, options|
- MergeRequests::PipelineEntity.represent(merge_request.merge_pipeline, options)
- end
-
expose :default_merge_commit_message
expose :mergeable do |merge_request, options|
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index c3e8b66fe12..1c033dee5ff 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -110,9 +110,17 @@ class MergeRequestWidgetEntity < Grape::Entity
presenter(merge_request).closing_issues_links
end
+ expose :closing_count do |merge_request|
+ presenter(merge_request).closing_issues.size
+ end
+
expose :mentioned_but_not_closing do |merge_request|
presenter(merge_request).mentioned_issues_links
end
+
+ expose :mentioned_count do |merge_request|
+ presenter(merge_request).mentioned_issues.size
+ end
end
expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do
@@ -137,6 +145,23 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.enabled_reports
end
+ expose :show_gitpod_button do |merge_request|
+ Gitlab::CurrentSettings.gitpod_enabled
+ end
+
+ expose :gitpod_url do |merge_request|
+ next unless Gitlab::CurrentSettings.gitpod_enabled
+
+ gitpod_url = Gitlab::CurrentSettings.gitpod_url
+ context_url = project_merge_request_url(merge_request.project, merge_request)
+
+ "#{gitpod_url}##{context_url}"
+ end
+
+ expose :gitpod_enabled do |merge_request|
+ current_user&.gitpod_enabled || false
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb
index a2a9e7375a0..c656cff9dd7 100644
--- a/app/serializers/paginated_diff_entity.rb
+++ b/app/serializers/paginated_diff_entity.rb
@@ -17,7 +17,7 @@ class PaginatedDiffEntity < Grape::Entity
options.merge(
submodule_links: submodule_links,
code_navigation_path: code_navigation_path(diffs),
- conflicts: conflicts
+ conflicts: conflicts(allow_tree_conflicts: options[:allow_tree_conflicts])
)
)
end
diff --git a/app/services/admin/propagate_service_template.rb b/app/services/admin/propagate_service_template.rb
index 07be3c1027d..c251537c479 100644
--- a/app/services/admin/propagate_service_template.rb
+++ b/app/services/admin/propagate_service_template.rb
@@ -5,9 +5,7 @@ module Admin
include PropagateService
def propagate
- return unless integration.active?
-
- create_integration_for_projects_without_integration
+ # TODO: Remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/335178
end
end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index d42dcb2fd00..a2683647c72 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -21,7 +21,7 @@ module Auth
return error('DENIED', status: 403, message: 'access forbidden') unless has_registry_ability?
- unless scopes.any? || current_user || project
+ unless scopes.any? || current_user || deploy_token || project
return error('DENIED', status: 403, message: 'access forbidden')
end
@@ -124,7 +124,6 @@ module Auth
end
def migration_eligible(project, actions)
- return unless actions.include?('push')
return unless Feature.enabled?(:container_registry_migration_phase1)
# The migration process will start by allowing only specific test and gitlab-org projects using the
@@ -178,8 +177,7 @@ module Auth
end
def can_user?(ability, project)
- user = current_user.is_a?(User) ? current_user : nil
- can?(user, ability, project)
+ can?(current_user, ability, project)
end
def build_can_pull?(requested_project)
@@ -202,16 +200,16 @@ module Auth
def deploy_token_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
- current_user.is_a?(DeployToken) &&
- current_user.has_access_to?(requested_project) &&
- current_user.read_registry?
+ deploy_token.present? &&
+ deploy_token.has_access_to?(requested_project) &&
+ deploy_token.read_registry?
end
def deploy_token_can_push?(requested_project)
has_authentication_ability?(:create_container_image) &&
- current_user.is_a?(DeployToken) &&
- current_user.has_access_to?(requested_project) &&
- current_user.write_registry?
+ deploy_token.present? &&
+ deploy_token.has_access_to?(requested_project) &&
+ deploy_token.write_registry?
end
##
@@ -250,6 +248,10 @@ module Auth
{}
end
+ def deploy_token
+ params[:deploy_token]
+ end
+
def log_if_actions_denied(type, requested_project, requested_actions, authorized_actions)
return if requested_actions == authorized_actions
diff --git a/app/services/auth/dependency_proxy_authentication_service.rb b/app/services/auth/dependency_proxy_authentication_service.rb
index fab42e0ebb6..164594d6f6c 100644
--- a/app/services/auth/dependency_proxy_authentication_service.rb
+++ b/app/services/auth/dependency_proxy_authentication_service.rb
@@ -8,10 +8,7 @@ module Auth
def execute(authentication_abilities:)
return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled
-
- # Because app/controllers/concerns/dependency_proxy/auth.rb consumes this
- # JWT only as `User.find`, we currently only allow User (not DeployToken, etc)
- return error('access forbidden', 403) unless current_user.is_a?(User)
+ return error('access forbidden', 403) unless valid_user_actor?
{ token: authorized_token.encoded }
end
@@ -36,11 +33,24 @@ module Auth
private
+ def valid_user_actor?
+ current_user || valid_deploy_token?
+ end
+
+ def valid_deploy_token?
+ deploy_token && deploy_token.valid_for_dependency_proxy?
+ end
+
def authorized_token
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
- token['user_id'] = current_user.id
+ token['user_id'] = current_user.id if current_user
+ token['deploy_token'] = deploy_token.token if deploy_token
token.expire_time = self.class.token_expire_at
end
end
+
+ def deploy_token
+ params[:deploy_token]
+ end
end
end
diff --git a/app/services/authorized_project_update/project_recalculate_per_user_service.rb b/app/services/authorized_project_update/project_recalculate_per_user_service.rb
new file mode 100644
index 00000000000..6141bfc6498
--- /dev/null
+++ b/app/services/authorized_project_update/project_recalculate_per_user_service.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectRecalculatePerUserService < ProjectRecalculateService
+ def initialize(project, user)
+ @project = project
+ @user = user
+ end
+
+ private
+
+ attr_reader :user
+
+ def apply_scopes(project_authorizations)
+ super.where(user_id: user.id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def effective_access_levels
+ Projects::Members::EffectiveAccessLevelPerUserFinder.new(project, user).execute
+ end
+ end
+end
diff --git a/app/services/authorized_project_update/project_recalculate_service.rb b/app/services/authorized_project_update/project_recalculate_service.rb
index cbb8efaf99f..d70d0efc2af 100644
--- a/app/services/authorized_project_update/project_recalculate_service.rb
+++ b/app/services/authorized_project_update/project_recalculate_service.rb
@@ -26,7 +26,7 @@ module AuthorizedProjectUpdate
def current_authorizations
strong_memoize(:current_authorizations) do
- project.project_authorizations
+ apply_scopes(project.project_authorizations)
.pluck(:user_id, :access_level) # rubocop: disable CodeReuse/ActiveRecord
end
end
@@ -35,8 +35,7 @@ module AuthorizedProjectUpdate
strong_memoize(:fresh_authorizations) do
result = []
- Projects::Members::EffectiveAccessLevelFinder.new(project)
- .execute
+ effective_access_levels
.each_batch(of: BATCH_SIZE, column: :user_id) do |member_batch|
result += member_batch.pluck(:user_id, 'MAX(access_level)') # rubocop: disable CodeReuse/ActiveRecord
end
@@ -76,5 +75,13 @@ module AuthorizedProjectUpdate
end
end
end
+
+ def apply_scopes(project_authorizations)
+ project_authorizations
+ end
+
+ def effective_access_levels
+ Projects::Members::EffectiveAccessLevelFinder.new(project).execute
+ end
end
end
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 142eebca2e3..e756e8c14d8 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -6,7 +6,7 @@ module AutoMerge
include MergeRequests::AssignsMergeParams
def execute(merge_request)
- ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases
register_auto_merge_parameters!(merge_request)
yield if block_given?
end
@@ -29,7 +29,7 @@ module AutoMerge
end
def cancel(merge_request)
- ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases
clear_auto_merge_parameters!(merge_request)
yield if block_given?
end
@@ -41,7 +41,7 @@ module AutoMerge
end
def abort(merge_request, reason)
- ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases
clear_auto_merge_parameters!(merge_request)
yield if block_given?
end
diff --git a/app/services/boards/base_item_move_service.rb b/app/services/boards/base_item_move_service.rb
index dfd0002cbc9..be16c595abb 100644
--- a/app/services/boards/base_item_move_service.rb
+++ b/app/services/boards/base_item_move_service.rb
@@ -4,13 +4,23 @@ module Boards
class BaseItemMoveService < Boards::BaseService
def execute(issuable)
issuable_modification_params = issuable_params(issuable)
- return false if issuable_modification_params.empty?
+ return if issuable_modification_params.empty?
- move_single_issuable(issuable, issuable_modification_params)
+ return unless move_single_issuable(issuable, issuable_modification_params)
+
+ success(issuable)
end
private
+ def success(issuable)
+ ServiceResponse.success(payload: { issuable: issuable })
+ end
+
+ def error(issuable, message)
+ ServiceResponse.error(payload: { issuable: issuable }, message: message)
+ end
+
def issuable_params(issuable)
attrs = {}
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 0e95bf7a434..8806e6788ff 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -42,7 +42,7 @@ module Boards
end
def set_issue_types
- params[:issue_types] = Issue::TYPES_FOR_LIST
+ params[:issue_types] ||= Issue::TYPES_FOR_LIST
end
def item_model
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
index fc1580ab880..45465ba3946 100644
--- a/app/services/bulk_update_integration_service.rb
+++ b/app/services/bulk_update_integration_service.rb
@@ -9,10 +9,10 @@ class BulkUpdateIntegrationService
# rubocop: disable CodeReuse/ActiveRecord
def execute
Integration.transaction do
- Integration.where(id: batch.select(:id)).update_all(integration_hash)
+ Integration.where(id: batch_ids).update_all(integration_hash)
if integration.data_fields_present?
- integration.data_fields.class.where(service_id: batch.select(:id)).update_all(data_fields_hash)
+ integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash)
end
end
end
@@ -29,4 +29,13 @@ class BulkUpdateIntegrationService
def data_fields_hash
integration.to_data_fields_hash
end
+
+ def batch_ids
+ @batch_ids ||=
+ if batch.is_a?(ActiveRecord::Relation)
+ batch.select(:id)
+ else
+ batch.map(&:id)
+ end
+ end
end
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb
index b422e57baad..f717dd0862c 100644
--- a/app/services/ci/after_requeue_job_service.rb
+++ b/app/services/ci/after_requeue_job_service.rb
@@ -38,7 +38,7 @@ module Ci
end
def stage_dependent_jobs(processable)
- skipped_jobs(processable).scheduling_type_stage.after_stage(processable.stage_idx)
+ skipped_jobs(processable).after_stage(processable.stage_idx)
end
def needs_dependent_jobs(processable)
diff --git a/app/services/ci/append_build_trace_service.rb b/app/services/ci/append_build_trace_service.rb
index 8200f9790ee..0eef0ff0e61 100644
--- a/app/services/ci/append_build_trace_service.rb
+++ b/app/services/ci/append_build_trace_service.rb
@@ -71,8 +71,7 @@ module Ci
end
def trace_size_exceeded?(size)
- Feature.enabled?(:ci_jobs_trace_size_limit, project, default_enabled: :yaml) &&
- project.actual_limits.exceeded?(:ci_jobs_trace_size_limit, size / 1.megabyte)
+ project.actual_limits.exceeded?(:ci_jobs_trace_size_limit, size / 1.megabyte)
end
end
end
diff --git a/app/services/ci/build_cancel_service.rb b/app/services/ci/build_cancel_service.rb
new file mode 100644
index 00000000000..a23418ed738
--- /dev/null
+++ b/app/services/ci/build_cancel_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildCancelService
+ def initialize(build, user)
+ @build = build
+ @user = user
+ end
+
+ def execute
+ return forbidden unless allowed?
+ return unprocessable_entity unless build.cancelable?
+
+ build.cancel
+
+ ServiceResponse.success(payload: build)
+ end
+
+ private
+
+ attr_reader :build, :user
+
+ def allowed?
+ user.can?(:update_build, build)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
+ end
+
+ def unprocessable_entity
+ ServiceResponse.error(message: 'Unprocessable entity', http_status: :unprocessable_entity)
+ end
+ end
+end
diff --git a/app/services/ci/build_unschedule_service.rb b/app/services/ci/build_unschedule_service.rb
new file mode 100644
index 00000000000..3b367ecc4c4
--- /dev/null
+++ b/app/services/ci/build_unschedule_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildUnscheduleService
+ def initialize(build, user)
+ @build = build
+ @user = user
+ end
+
+ def execute
+ return forbidden unless allowed?
+ return unprocessable_entity unless build.scheduled?
+
+ build.unschedule!
+
+ ServiceResponse.success(payload: build)
+ end
+
+ private
+
+ attr_reader :build, :user
+
+ def allowed?
+ user.can?(:update_build, build)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
+ end
+
+ def unprocessable_entity
+ ServiceResponse.error(message: 'Unprocessable entity', http_status: :unprocessable_entity)
+ end
+ end
+end
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index e9ec2338171..a65c22e273c 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -33,8 +33,9 @@ module Ci
current_user,
pipeline_params.fetch(:target_revision))
- downstream_pipeline = service.execute(
- pipeline_params.fetch(:source), **pipeline_params[:execute_params])
+ downstream_pipeline = service
+ .execute(pipeline_params.fetch(:source), **pipeline_params[:execute_params])
+ .payload
downstream_pipeline.tap do |pipeline|
update_bridge_status!(@bridge, pipeline)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index c039f31aafc..ba9665555cc 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -11,11 +11,11 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::SecurityOrchestrationPolicy,
+ Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Config::Content,
Gitlab::Ci::Pipeline::Chain::Config::Process,
Gitlab::Ci::Pipeline::Chain::Validate::AfterConfig,
Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
- Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::SeedBlock,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Seed,
@@ -87,12 +87,16 @@ module Ci
if pipeline.persisted?
schedule_head_pipeline_update
create_namespace_onboarding_action
+ else
+ # If pipeline is not persisted, try to recover IID
+ pipeline.reset_project_iid
end
- # If pipeline is not persisted, try to recover IID
- pipeline.reset_project_iid unless pipeline.persisted?
-
- pipeline
+ if error_message = pipeline.full_error_messages.presence || pipeline.failure_reason.presence
+ ServiceResponse.error(message: error_message, payload: pipeline)
+ else
+ ServiceResponse.success(payload: pipeline)
+ end
end
# rubocop: enable Metrics/ParameterLists
@@ -100,8 +104,8 @@ module Ci
source = args[0]
params = Hash(args[1])
- execute(source, **params, &block).tap do |pipeline|
- unless pipeline.persisted?
+ execute(source, **params, &block).tap do |response|
+ unless response.payload.persisted?
raise CreateError, pipeline.full_error_messages
end
end
diff --git a/app/services/ci/daily_build_group_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb
index 820e6e08fc5..25c6d57d961 100644
--- a/app/services/ci/daily_build_group_report_result_service.rb
+++ b/app/services/ci/daily_build_group_report_result_service.rb
@@ -3,7 +3,13 @@
module Ci
class DailyBuildGroupReportResultService
def execute(pipeline)
- DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline))
+ if DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline))
+ Projects::CiFeatureUsage.insert_usage(
+ project_id: pipeline.project_id,
+ feature: :code_coverage,
+ default_branch: pipeline.default_branch?
+ )
+ end
end
private
diff --git a/app/services/ci/delete_unit_tests_service.rb b/app/services/ci/delete_unit_tests_service.rb
index 28f96351175..230661a107d 100644
--- a/app/services/ci/delete_unit_tests_service.rb
+++ b/app/services/ci/delete_unit_tests_service.rb
@@ -23,7 +23,7 @@ module Ci
def delete_batch!(klass)
deleted = 0
- ActiveRecord::Base.transaction do
+ klass.transaction do
ids = klass.deletable.lock('FOR UPDATE SKIP LOCKED').limit(BATCH_SIZE).pluck(:id)
break if ids.empty?
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index 494fcb23a06..dd5c8e0379f 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -7,7 +7,7 @@ module Ci
Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true)
- pipeline.cancel_running if pipeline.cancelable? && ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, default_enabled: :yaml)
+ pipeline.cancel_running if pipeline.cancelable?
pipeline.reset.destroy!
diff --git a/app/services/ci/drop_pipeline_service.rb b/app/services/ci/drop_pipeline_service.rb
index f510943251b..16d3abcbfa0 100644
--- a/app/services/ci/drop_pipeline_service.rb
+++ b/app/services/ci/drop_pipeline_service.rb
@@ -2,6 +2,9 @@
module Ci
class DropPipelineService
+ PRELOADED_COMMIT_STATUS_RELATIONS = [:project, :pipeline].freeze
+ PRELOADED_CI_BUILD_RELATIONS = [:metadata, :deployment, :taggings].freeze
+
# execute service asynchronously for each cancelable pipeline
def execute_async_for_all(pipelines, failure_reason, context_user)
pipelines.cancelable.select(:id).find_in_batches do |pipelines_batch|
@@ -27,11 +30,11 @@ module Ci
private
- def preload_associations_for_drop(builds_batch)
- ActiveRecord::Associations::Preloader.new.preload( # rubocop: disable CodeReuse/ActiveRecord
- builds_batch,
- [:project, :pipeline, :metadata, :deployment, :taggings]
- )
+ # rubocop: disable CodeReuse/ActiveRecord
+ def preload_associations_for_drop(commit_status_batch)
+ ActiveRecord::Associations::Preloader.new.preload(commit_status_batch, PRELOADED_COMMIT_STATUS_RELATIONS)
+ ActiveRecord::Associations::Preloader.new.preload(commit_status_batch.select { |job| job.is_a?(Ci::Build) }, PRELOADED_CI_BUILD_RELATIONS)
end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/ci/external_pull_requests/create_pipeline_service.rb b/app/services/ci/external_pull_requests/create_pipeline_service.rb
index 78be94bfb41..83499524a8e 100644
--- a/app/services/ci/external_pull_requests/create_pipeline_service.rb
+++ b/app/services/ci/external_pull_requests/create_pipeline_service.rb
@@ -7,7 +7,8 @@ module Ci
module ExternalPullRequests
class CreatePipelineService < BaseService
def execute(pull_request)
- return unless pull_request.open? && pull_request.actual_branch_head?
+ return pull_request_not_open_error unless pull_request.open?
+ return pull_request_branch_error unless pull_request.actual_branch_head?
create_pipeline_for(pull_request)
end
@@ -26,6 +27,14 @@ module Ci
target_sha: pull_request.target_sha
}
end
+
+ def pull_request_not_open_error
+ ServiceResponse.error(message: 'The pull request is not opened', payload: nil)
+ end
+
+ def pull_request_branch_error
+ ServiceResponse.error(message: 'The source sha is not the head of the source branch', payload: nil)
+ end
end
end
end
diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb
deleted file mode 100644
index c756e376901..00000000000
--- a/app/services/ci/extract_sections_from_build_trace_service.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class ExtractSectionsFromBuildTraceService < BaseService
- def execute(build)
- return false unless build.trace_sections.empty?
-
- Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) # rubocop:disable Gitlab/BulkInsert
- true
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def find_or_create_name(name)
- project.build_trace_section_names.find_or_create_by!(name: name)
- rescue ActiveRecord::RecordInvalid
- project.build_trace_section_names.find_by!(name: name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def extract_sections(build)
- build.trace.extract_sections.map do |attr|
- name = attr.delete(:name)
- name_record = find_or_create_name(name)
-
- attr.merge(
- build_id: build.id,
- project_id: project.id,
- section_name_id: name_record.id)
- end
- end
- end
-end
diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb
index 62c4d6b4599..7746382b845 100644
--- a/app/services/ci/pipeline_trigger_service.rb
+++ b/app/services/ci/pipeline_trigger_service.rb
@@ -27,13 +27,13 @@ module Ci
# this check is to not leak the presence of the project if user cannot read it
return unless trigger.project == project
- pipeline = Ci::CreatePipelineService
+ response = Ci::CreatePipelineService
.new(project, trigger.owner, ref: params[:ref], variables_attributes: variables)
.execute(:trigger, ignore_skip_ci: true) do |pipeline|
pipeline.trigger_requests.build(trigger: trigger)
end
- pipeline_service_response(pipeline)
+ pipeline_service_response(response.payload)
end
def pipeline_service_response(pipeline)
@@ -57,7 +57,7 @@ module Ci
# this check is to not leak the presence of the project if user cannot read it
return unless can?(job.user, :read_project, project)
- pipeline = Ci::CreatePipelineService
+ response = Ci::CreatePipelineService
.new(project, job.user, ref: params[:ref], variables_attributes: variables)
.execute(:pipeline, ignore_skip_ci: true) do |pipeline|
source = job.sourced_pipelines.build(
@@ -69,7 +69,7 @@ module Ci
pipeline.source_pipeline = source
end
- pipeline_service_response(pipeline)
+ pipeline_service_response(response.payload)
end
def job_from_token
diff --git a/app/services/ci/pipelines/add_job_service.rb b/app/services/ci/pipelines/add_job_service.rb
index 03bdb491200..41f9903e48c 100644
--- a/app/services/ci/pipelines/add_job_service.rb
+++ b/app/services/ci/pipelines/add_job_service.rb
@@ -3,21 +3,33 @@
module Ci
module Pipelines
class AddJobService
+ include ::Gitlab::ExclusiveLeaseHelpers
+
attr_reader :pipeline
def initialize(pipeline)
@pipeline = pipeline
- raise ArgumentError, "Pipeline must be persisted for this service to be used" unless @pipeline.persisted?
+ raise ArgumentError, "Pipeline must be persisted for this service to be used" unless pipeline.persisted?
end
def execute!(job, &block)
assign_pipeline_attributes(job)
- Ci::Pipeline.transaction do
- yield(job)
+ if Feature.enabled?(:ci_pipeline_add_job_with_lock, pipeline.project, default_enabled: :yaml)
+ in_lock("ci:pipelines:#{pipeline.id}:add-job", ttl: LOCK_TIMEOUT, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES) do
+ Ci::Pipeline.transaction do
+ yield(job)
+
+ job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, pipeline.project, default_enabled: :yaml)
+ end
+ end
+ else
+ Ci::Pipeline.transaction do
+ yield(job)
- job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, @pipeline.project, default_enabled: :yaml)
+ job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, pipeline.project, default_enabled: :yaml)
+ end
end
ServiceResponse.success(payload: { job: job })
@@ -27,10 +39,14 @@ module Ci
private
+ LOCK_TIMEOUT = 1.minute
+ LOCK_SLEEP = 0.5.seconds
+ LOCK_RETRIES = 20
+
def assign_pipeline_attributes(job)
- job.pipeline = @pipeline
- job.project = @pipeline.project
- job.ref = @pipeline.ref
+ job.pipeline = pipeline
+ job.project = pipeline.project
+ job.ref = pipeline.ref
end
end
end
diff --git a/app/services/ci/queue/builds_table_strategy.rb b/app/services/ci/queue/builds_table_strategy.rb
index c941734ac40..d0a343cb9d4 100644
--- a/app/services/ci/queue/builds_table_strategy.rb
+++ b/app/services/ci/queue/builds_table_strategy.rb
@@ -53,6 +53,14 @@ module Ci
relation.pluck(:id)
end
+ def use_denormalized_shared_runners_data?
+ false
+ end
+
+ def use_denormalized_minutes_data?
+ false
+ end
+
private
def running_builds_for_shared_runners
diff --git a/app/services/ci/queue/pending_builds_strategy.rb b/app/services/ci/queue/pending_builds_strategy.rb
index 55d5cb96a0a..efe3a981d3a 100644
--- a/app/services/ci/queue/pending_builds_strategy.rb
+++ b/app/services/ci/queue/pending_builds_strategy.rb
@@ -11,25 +11,9 @@ module Ci
# rubocop:disable CodeReuse/ActiveRecord
def builds_for_shared_runner
- relation = new_builds
- # don't run projects which have not enabled shared runners and builds
- .joins('INNER JOIN projects ON ci_pending_builds.project_id = projects.id')
- .where(projects: { shared_runners_enabled: true, pending_delete: false })
- .joins('LEFT JOIN project_features ON ci_pending_builds.project_id = project_features.project_id')
- .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0')
+ shared_builds = builds_available_for_shared_runners
- if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml)
- # if disaster recovery is enabled, we fallback to FIFO scheduling
- relation.order('ci_pending_builds.build_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
- .with(running_builds_for_shared_runners_cte.to_arel)
- .joins("LEFT JOIN project_builds ON ci_pending_builds.project_id = project_builds.project_id")
- .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_pending_builds.build_id ASC')
- end
+ builds_ordered_for_shared_runners(shared_builds)
end
def builds_matching_tag_ids(relation, ids)
@@ -52,8 +36,44 @@ module Ci
relation.pluck(:build_id)
end
+ def use_denormalized_shared_runners_data?
+ ::Feature.enabled?(:ci_queueing_denormalize_shared_runners_information, runner, type: :development, default_enabled: :yaml)
+ end
+
+ def use_denormalized_minutes_data?
+ ::Feature.enabled?(:ci_queueing_denormalize_ci_minutes_information, runner, type: :development, default_enabled: :yaml)
+ end
+
private
+ def builds_available_for_shared_runners
+ if use_denormalized_shared_runners_data?
+ new_builds.with_instance_runners
+ else
+ new_builds
+ # don't run projects which have not enabled shared runners and builds
+ .joins('INNER JOIN projects ON ci_pending_builds.project_id = projects.id')
+ .where(projects: { shared_runners_enabled: true, pending_delete: false })
+ .joins('LEFT JOIN project_features ON ci_pending_builds.project_id = project_features.project_id')
+ .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0')
+ end
+ end
+
+ def builds_ordered_for_shared_runners(relation)
+ if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml)
+ # if disaster recovery is enabled, we fallback to FIFO scheduling
+ relation.order('ci_pending_builds.build_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
+ .with(running_builds_for_shared_runners_cte.to_arel)
+ .joins("LEFT JOIN project_builds ON ci_pending_builds.project_id = project_builds.project_id")
+ .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_pending_builds.build_id ASC')
+ end
+ end
+
def running_builds_for_shared_runners_cte
running_builds = ::Ci::RunningBuild
.instance_type
@@ -67,3 +87,5 @@ module Ci
end
end
end
+
+Ci::Queue::PendingBuildsStrategy.prepend_mod_with('Ci::Queue::PendingBuildsStrategy')
diff --git a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
index 9e3e6de3928..1d329fe7b53 100644
--- a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
+++ b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
@@ -5,6 +5,8 @@ module Ci
class AssignResourceFromResourceGroupService < ::BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(resource_group)
+ release_resource_from_stale_jobs(resource_group)
+
free_resources = resource_group.resources.free.count
resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
@@ -12,6 +14,14 @@ module Ci
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def release_resource_from_stale_jobs(resource_group)
+ resource_group.resources.stale_processables.find_each do |processable|
+ resource_group.release_resource_from(processable)
+ end
+ end
end
end
end
diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb
index 07faf90dd6d..7c169cb8395 100644
--- a/app/services/ci/unlock_artifacts_service.rb
+++ b/app/services/ci/unlock_artifacts_service.rb
@@ -17,7 +17,7 @@ module Ci
SQL
loop do
- break if ActiveRecord::Base.connection.exec_query(query).empty?
+ break if Ci::Pipeline.connection.exec_query(query).empty?
end
end
diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb
index bd06f9d7628..f3dbf31dcdb 100644
--- a/app/services/dependency_proxy/find_or_create_blob_service.rb
+++ b/app/services/dependency_proxy/find_or_create_blob_service.rb
@@ -10,10 +10,12 @@ module DependencyProxy
end
def execute
+ from_cache = true
file_name = @blob_sha.sub('sha256:', '') + '.gz'
blob = @group.dependency_proxy_blobs.find_or_build(file_name)
unless blob.persisted?
+ from_cache = false
result = DependencyProxy::DownloadBlobService
.new(@image, @blob_sha, @token).execute
@@ -28,7 +30,7 @@ module DependencyProxy
blob.save!
end
- success(blob: blob)
+ success(blob: blob, from_cache: from_cache)
end
private
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index ee608d715aa..0eb990ab7f8 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -17,10 +17,10 @@ module DependencyProxy
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
- return success(manifest: @manifest) if cached_manifest_matches?(head_result)
+ return success(manifest: @manifest, from_cache: true) if cached_manifest_matches?(head_result)
pull_new_manifest
- respond
+ respond(from_cache: false)
rescue Timeout::Error, *Gitlab::HTTP::HTTP_ERRORS
respond
end
@@ -44,9 +44,9 @@ module DependencyProxy
@manifest && @manifest.digest == head_result[:digest] && @manifest.content_type == head_result[:content_type]
end
- def respond
+ def respond(from_cache: true)
if @manifest.persisted?
- success(manifest: @manifest)
+ success(manifest: @manifest, from_cache: from_cache)
else
error('Failed to download the manifest from the external registry', 503)
end
diff --git a/app/services/dependency_proxy/pull_manifest_service.rb b/app/services/dependency_proxy/pull_manifest_service.rb
index 737414c396e..31494773cc0 100644
--- a/app/services/dependency_proxy/pull_manifest_service.rb
+++ b/app/services/dependency_proxy/pull_manifest_service.rb
@@ -17,7 +17,7 @@ module DependencyProxy
file = Tempfile.new
begin
- file.write(response)
+ file.write(response.body)
file.flush
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
diff --git a/app/services/deployments/update_environment_service.rb b/app/services/deployments/update_environment_service.rb
index 6f85779c285..83c37257297 100644
--- a/app/services/deployments/update_environment_service.rb
+++ b/app/services/deployments/update_environment_service.rb
@@ -26,7 +26,7 @@ module Deployments
end
def update_environment(deployment)
- ActiveRecord::Base.transaction do
+ ActiveRecord::Base.transaction do # rubocop: disable Database/MultipleDatabases
# Renew attributes at update
renew_external_url
renew_auto_stop_in
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 b40f6a81174..c43696442d2 100644
--- a/app/services/design_management/copy_design_collection/copy_service.rb
+++ b/app/services/design_management/copy_design_collection/copy_service.rb
@@ -36,7 +36,7 @@ module DesignManagement
with_temporary_branch do
copy_commits!
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
design_ids = copy_designs!
version_ids = copy_versions!
copy_actions!(design_ids, version_ids)
@@ -181,12 +181,12 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
# When this is fixed, we can remove the call to
# `with_project_iid_supply` above, since the objects will be instantiated
# and callbacks (including `ensure_project_iid!`) will fire.
- ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Design.table_name,
new_rows,
return_ids: true
@@ -207,9 +207,9 @@ module DesignManagement
)
end
- # TODO Replace `Gitlab::Database.bulk_insert` with `BulkInsertSafe`
+ # TODO Replace `Gitlab::Database.main.bulk_insert` with `BulkInsertSafe`
# once https://gitlab.com/gitlab-org/gitlab/-/issues/247718 is fixed.
- ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Version.table_name,
new_rows,
return_ids: true
@@ -239,7 +239,7 @@ module DesignManagement
end
# We cannot use `BulkInsertSafe` because of the uploader mounted in `Action`.
- ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
DesignManagement::Action.table_name,
new_rows
)
@@ -278,7 +278,7 @@ module DesignManagement
# We cannot use `BulkInsertSafe` due to the LfsObjectsProject#update_project_statistics
# callback that fires after_commit.
- ::Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
LfsObjectsProject.table_name,
new_rows,
on_conflict: :do_nothing # Upsert
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index d73c3417a8b..3a1db16aaf4 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -32,26 +32,28 @@ module DraftNotes
review = Review.create!(author: current_user, merge_request: merge_request, project: project)
- draft_notes.map do |draft_note|
+ created_notes = draft_notes.map do |draft_note|
draft_note.review = review
- create_note_from_draft(draft_note)
+ create_note_from_draft(draft_note, skip_capture_diff_note_position: true)
end
- draft_notes.delete_all
+ capture_diff_note_positions(created_notes)
+ draft_notes.delete_all
set_reviewed
-
notification_service.async.new_review(review)
MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
end
- def create_note_from_draft(draft)
+ def create_note_from_draft(draft, skip_capture_diff_note_position: false)
# Make sure the diff file is unfolded in order to find the correct line
# codes.
draft.diff_file&.unfold_diff_lines(draft.original_position)
- note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute
- set_discussion_resolve_status(note, draft)
+ note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute(
+ skip_capture_diff_note_position: skip_capture_diff_note_position
+ )
+ set_discussion_resolve_status(note, draft)
note
end
@@ -70,5 +72,19 @@ module DraftNotes
def set_reviewed
::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
end
+
+ def capture_diff_note_positions(notes)
+ paths = notes.flat_map do |note|
+ note.diff_file&.paths if note.diff_note?
+ end
+
+ return if paths.empty?
+
+ capture_service = Discussions::CaptureDiffNotePositionService.new(merge_request, paths.compact)
+
+ notes.each do |note|
+ capture_service.execute(note.discussion) if note.diff_note? && note.start_of_discussion?
+ end
+ end
end
end
diff --git a/app/services/environments/auto_stop_service.rb b/app/services/environments/auto_stop_service.rb
index bde598abf66..4e3aec64283 100644
--- a/app/services/environments/auto_stop_service.rb
+++ b/app/services/environments/auto_stop_service.rb
@@ -32,7 +32,7 @@ module Environments
return false unless environments.exists?
- Ci::StopEnvironmentsService.execute_in_batch(environments)
+ Environments::StopService.execute_in_batch(environments)
end
end
end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/environments/stop_service.rb
index 7c9fc44e7f4..089aea11296 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/environments/stop_service.rb
@@ -1,19 +1,25 @@
# frozen_string_literal: true
-module Ci
- class StopEnvironmentsService < BaseService
+module Environments
+ class StopService < BaseService
attr_reader :ref
- def execute(branch_name)
+ def execute(environment)
+ return unless can?(current_user, :stop_environment, environment)
+
+ environment.stop_with_action!(current_user)
+ end
+
+ def execute_for_branch(branch_name)
@ref = branch_name
return unless @ref.present?
- environments.each { |environment| stop(environment) }
+ environments.each { |environment| execute(environment) }
end
def execute_for_merge_request(merge_request)
- merge_request.environments.each { |environment| stop(environment) }
+ merge_request.environments.each { |environment| execute(environment) }
end
##
@@ -39,12 +45,5 @@ module Ci
.new(project, current_user, ref: @ref, recently_updated: true)
.execute
end
-
- def stop(environment)
- return unless environment.stop_action_available?
- return unless can?(current_user, :stop_environment, environment)
-
- environment.stop_with_action!(current_user)
- end
end
end
diff --git a/app/services/error_tracking/issue_details_service.rb b/app/services/error_tracking/issue_details_service.rb
index 0068a9e9b6d..1614c597a8e 100644
--- a/app/services/error_tracking/issue_details_service.rb
+++ b/app/services/error_tracking/issue_details_service.rb
@@ -8,7 +8,7 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
+ response = find_issue_details(params[:issue_id])
compose_response(response) do
# The gitlab_issue attribute can contain an absolute GitLab url from the Sentry Client
@@ -36,5 +36,29 @@ module ErrorTracking
def parse_response(response)
{ issue: response[:issue] }
end
+
+ def find_issue_details(issue_id)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(issue_id)
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ issue: error.to_sentry_detailed_error
+ }
+ else
+ project_error_tracking_setting.issue_details(issue_id: issue_id)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/issue_latest_event_service.rb b/app/services/error_tracking/issue_latest_event_service.rb
index a39f1cde1b2..1bf86c658fc 100644
--- a/app/services/error_tracking/issue_latest_event_service.rb
+++ b/app/services/error_tracking/issue_latest_event_service.rb
@@ -5,7 +5,7 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
+ response = find_issue_latest_event(params[:issue_id])
compose_response(response)
end
@@ -13,5 +13,30 @@ module ErrorTracking
def parse_response(response)
{ latest_event: response[:latest_event] }
end
+
+ def find_issue_latest_event(issue_id)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(issue_id)
+ event = error.events.last
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ latest_event: event.to_sentry_error_event
+ }
+ else
+ project_error_tracking_setting.issue_latest_event(issue_id: issue_id)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb
index 2f8bbfddef0..624e5f94dde 100644
--- a/app/services/error_tracking/issue_update_service.rb
+++ b/app/services/error_tracking/issue_update_service.rb
@@ -5,10 +5,12 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.update_issue(
+ update_opts = {
issue_id: params[:issue_id],
params: update_params
- )
+ }
+
+ response = update_issue(update_opts)
compose_response(response) do
project_error_tracking_setting.expire_issues_cache
@@ -69,5 +71,31 @@ module ErrorTracking
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_update?
end
+
+ def update_issue(opts)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(opts[:issue_id])
+ error.status = opts[:params][:status]
+ error.save!
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ updated: true
+ }
+ else
+ project_error_tracking_setting.update_issue(**opts)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index 7087e3825d6..5ddba748fd4 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -22,13 +22,15 @@ module ErrorTracking
def perform
return invalid_status_error unless valid_status?
- response = project_error_tracking_setting.list_sentry_issues(
+ sentry_opts = {
issue_status: issue_status,
limit: limit,
search_term: params[:search_term].presence,
sort: sort,
cursor: params[:cursor].presence
- )
+ }
+
+ response = list_issues(sentry_opts)
compose_response(response)
end
@@ -56,5 +58,36 @@ module ErrorTracking
def sort
params[:sort] || DEFAULT_SORT
end
+
+ def list_issues(opts)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ # We are going to support more options in the future.
+ # For now we implement the bare minimum for rendering the list in UI.
+ filter_opts = {
+ status: opts[:issue_status]
+ }
+
+ errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ issues: errors.map(&:to_sentry_error),
+ pagination: {}
+ }
+ else
+ project_error_tracking_setting.list_sentry_issues(**opts)
+ end
+ end
end
end
diff --git a/app/services/feature_flags/create_service.rb b/app/services/feature_flags/create_service.rb
index 5c87af561d5..5111f914447 100644
--- a/app/services/feature_flags/create_service.rb
+++ b/app/services/feature_flags/create_service.rb
@@ -6,7 +6,7 @@ module FeatureFlags
return error('Access Denied', 403) unless can_create?
return error('Version is invalid', :bad_request) unless valid_version?
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
feature_flag = project.operations_feature_flags.new(params)
if feature_flag.save
diff --git a/app/services/feature_flags/destroy_service.rb b/app/services/feature_flags/destroy_service.rb
index b131a349fc7..986fe004db6 100644
--- a/app/services/feature_flags/destroy_service.rb
+++ b/app/services/feature_flags/destroy_service.rb
@@ -11,7 +11,7 @@ module FeatureFlags
def destroy_feature_flag(feature_flag)
return error('Access Denied', 403) unless can_destroy?(feature_flag)
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
if feature_flag.destroy
save_audit_event(audit_event(feature_flag))
diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb
index f5ab6f4005b..01e4f661d75 100644
--- a/app/services/feature_flags/update_service.rb
+++ b/app/services/feature_flags/update_service.rb
@@ -12,7 +12,7 @@ module FeatureFlags
return error('Access Denied', 403) unless can_update?(feature_flag)
return error('Not Found', 404) unless valid_user_list_ids?(feature_flag, user_list_ids(params))
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
feature_flag.assign_attributes(params)
feature_flag.strategies.each do |strategy|
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 1eb54e13522..aee2f685e97 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -25,6 +25,7 @@ module Git
raise NotImplementedError, "Please implement #{self.class}##{__method__}"
end
+ # The changeset, ordered with the newest commit last
def commits
raise NotImplementedError, "Please implement #{self.class}##{__method__}"
end
@@ -132,10 +133,10 @@ module Git
end
def event_push_data
- # We only need the last commit for the event push, and we don't
+ # We only need the newest commit for the event push, and we don't
# need the full deltas either.
@event_push_data ||= Gitlab::DataBuilder::Push.build(
- **push_data_params(commits: commits.last, with_changed_files: false)
+ **push_data_params(commits: limited_commits.last, with_changed_files: false)
)
end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index a49b981c680..7a22d7ffcdf 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -21,8 +21,9 @@ module Git
def commits
strong_memoize(:commits) do
if creating_default_branch?
- # The most recent PROCESS_COMMIT_LIMIT commits in the default branch
- project.repository.commits(newrev, limit: PROCESS_COMMIT_LIMIT)
+ # The most recent PROCESS_COMMIT_LIMIT commits in the default branch.
+ # They are returned newest-to-oldest, but we need to present them oldest-to-newest
+ project.repository.commits(newrev, limit: PROCESS_COMMIT_LIMIT).reverse
elsif creating_branch?
# Use the pushed commits that aren't reachable by the default branch
# as a heuristic. This may include more commits than are actually
@@ -95,7 +96,7 @@ module Git
end
def track_ci_config_change_event
- return unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return unless ::ServicePing::ServicePingSettings.enabled?
return unless default_branch?
commits_changing_ci_config.each do |commit|
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index 5dcc2de456c..5bf39d98fa3 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -58,7 +58,7 @@ module Git
def stop_environments
return unless removing_branch?
- Ci::StopEnvironmentsService.new(project, current_user).execute(branch_name)
+ Environments::StopService.new(project, current_user).execute_for_branch(branch_name)
end
def unlock_artifacts
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 6f348ff9e0b..da05f18b5ac 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -51,7 +51,7 @@ module Git
change: change,
push_options: params[:push_options],
merge_request_branches: merge_request_branches,
- create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
+ create_pipelines: under_process_limit?(change),
execute_project_hooks: execute_project_hooks,
create_push_event: !create_bulk_push_event
).execute
@@ -60,6 +60,10 @@ module Git
create_bulk_push_event(ref_type, action, changes) if create_bulk_push_event
end
+ def under_process_limit?(change)
+ change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
+ end
+
def create_bulk_push_event(ref_type, action, changes)
EventCreateService.new.bulk_push(
project,
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 966d04ceb70..b7eae06b963 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -208,12 +208,12 @@ module Groups
end
def update_integrations
- @group.integrations.inherit.delete_all
+ @group.integrations.with_default_settings.delete_all
Integration.create_from_active_default_integrations(@group, :group_id)
end
def propagate_integrations
- @group.integrations.inherit.each do |integration|
+ @group.integrations.with_default_settings.each do |integration|
PropagateIntegrationWorker.perform_async(integration.id)
end
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 2f808d45ffd..2aaab88e778 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -111,7 +111,7 @@ module Import
private
def log_error(exception)
- Gitlab::Import::Logger.error(
+ Gitlab::GithubImport::Logger.error(
message: 'Import failed due to a GitHub error',
status: exception.response_status,
error: exception.response_body
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index e1b4613726d..d8b639bb422 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -99,7 +99,7 @@ module Issuable
yield(event)
end.compact
- Gitlab::Database.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(table_name, events) # rubocop:disable Gitlab/BulkInsert
end
end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 574fe85b466..1d2c5c06d1b 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -14,7 +14,7 @@ module Issuable
# Using transaction because of a high resources footprint
# on rewriting notes (unfolding references)
#
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
@new_entity = create_new_entity
update_new_entity
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index aedd0c377c6..38050708fc5 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -9,7 +9,7 @@ module Issuable
# We disable touch so that created system notes do not update
# the noteable's updated_at field
- ActiveRecord::Base.no_touching do
+ ApplicationRecord.no_touching do
if is_update
if issuable.previous_changes.include?('title')
create_title_change_note(issuable.previous_changes['title'].first)
diff --git a/app/services/issuable/destroy_label_links_service.rb b/app/services/issuable/destroy_label_links_service.rb
index 6fff9b5e8d2..49000d90c1f 100644
--- a/app/services/issuable/destroy_label_links_service.rb
+++ b/app/services/issuable/destroy_label_links_service.rb
@@ -22,7 +22,7 @@ module Issuable
SQL
loop do
- result = ActiveRecord::Base.connection.execute(delete_query)
+ result = LabelLink.connection.execute(delete_query)
break if result.cmd_tuples == 0
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 8d65865e7da..0984238517e 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -527,6 +527,12 @@ class IssuableBaseService < ::BaseProjectService
def allowed_update_params(params)
params
end
+
+ def update_issuable_sla(issuable)
+ return unless issuable_sla = issuable.issuable_sla
+
+ issuable_sla.update(issuable_closed: issuable.closed?)
+ end
end
IssuableBaseService.prepend_mod_with('IssuableBaseService')
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index cc4ad1a9c85..ea64239dd99 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -32,7 +32,7 @@ module Issues
notification_service.async.close_issue(issue, current_user, { closed_via: closed_via }) if notifications
todo_service.close_issue(issue, current_user)
- resolve_alert(issue)
+ perform_incident_management_actions(issue)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
@@ -51,6 +51,10 @@ module Issues
private
+ def perform_incident_management_actions(issue)
+ resolve_alert(issue)
+ end
+
def close_external_issue(issue, closed_via)
return unless project.external_issue_tracker&.support_close_issue?
@@ -89,3 +93,5 @@ module Issues
end
end
end
+
+Issues::CloseService.prepend_mod_with('Issues::CloseService')
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 30d081996b1..b15b3e49c9a 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -9,11 +9,6 @@ module Issues
# in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(project:, current_user: nil, params: {}, spam_params:)
- # Temporary check to ensure we are no longer passing request in params now that we have
- # introduced spam_params. Raise an exception if it is present.
- # Remove after https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58603 is complete.
- raise if params[:request]
-
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index e2b1b5400c7..977b924ed72 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -9,6 +9,7 @@ module Issues
event_service.reopen_issue(issue, current_user)
create_note(issue, 'reopened')
notification_service.async.reopen_issue(issue, current_user)
+ perform_incident_management_actions(issue)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
@@ -21,8 +22,13 @@ module Issues
private
+ def perform_incident_management_actions(issue)
+ end
+
def create_note(issue, state = issue.state)
SystemNoteService.change_status(issue, issue.project, current_user, state, nil)
end
end
end
+
+Issues::ReopenService.prepend_mod_with('Issues::ReopenService')
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index e4e2736ca2f..56484075d08 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -6,6 +6,17 @@ module Jira
include ProjectServicesLoggable
JIRA_API_VERSION = 2
+ # Limit the size of the JSON error message we will attempt to parse, as the JSON is external input.
+ JIRA_ERROR_JSON_SIZE_LIMIT = 5_000
+
+ ERRORS = {
+ connection: [Errno::ECONNRESET, Errno::ECONNREFUSED],
+ jira_ruby: JIRA::HTTPError,
+ ssl: OpenSSL::SSL::SSLError,
+ timeout: [Timeout::Error, Errno::ETIMEDOUT],
+ uri: [URI::InvalidURIError, SocketError]
+ }.freeze
+ ALL_ERRORS = ERRORS.values.flatten.freeze
def initialize(jira_integration, params = {})
@project = jira_integration&.project
@@ -43,15 +54,66 @@ module Jira
def request
response = client.get(url)
build_service_response(response)
- rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
- error_message = "Jira request error: #{error.message}"
- log_error("Error sending message", client_url: client.options[:site],
- error: {
- exception_class: error.class.name,
- exception_message: error.message,
- exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
- })
- ServiceResponse.error(message: error_message)
+ rescue *ALL_ERRORS => e
+ log_error('Error sending message',
+ client_url: client.options[:site],
+ error: {
+ exception_class: e.class.name,
+ exception_message: e.message,
+ exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace)
+ }
+ )
+
+ ServiceResponse.error(message: error_message(e))
+ end
+
+ def error_message(error)
+ reportable_error_message(error) ||
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ end
+
+ # Returns a user-facing error message if possible, otherwise `nil`.
+ def reportable_error_message(error)
+ case error
+ when ERRORS[:jira_ruby]
+ reportable_jira_ruby_error_message(error)
+ when ERRORS[:ssl]
+ s_('JiraRequest|An SSL error occurred while connecting to Jira: %{message}. Try your request again.') % { message: error.message }
+ when *ERRORS[:uri]
+ s_('JiraRequest|The Jira API URL for connecting to Jira is not valid. Check your Jira integration API URL and try again.')
+ when *ERRORS[:timeout]
+ s_('JiraRequest|A timeout error occurred while connecting to Jira. Try your request again.')
+ when *ERRORS[:connection]
+ s_('JiraRequest|A connection error occurred while connecting to Jira. Try your request again.')
+ end
+ end
+
+ # Returns a user-facing error message for a `JIRA::HTTPError` if possible,
+ # otherwise `nil`.
+ def reportable_jira_ruby_error_message(error)
+ case error.message
+ when 'Unauthorized'
+ s_('JiraRequest|The credentials for accessing Jira are not valid. Check your Jira integration credentials and try again.')
+ when 'Forbidden'
+ s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your Jira integration credentials and try again.')
+ when 'Bad Request'
+ s_('JiraRequest|An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ when /errorMessages/
+ jira_ruby_json_error_message(error.message)
+ end
+ end
+
+ def jira_ruby_json_error_message(error_message)
+ return if error_message.length > JIRA_ERROR_JSON_SIZE_LIMIT
+
+ begin
+ messages = Gitlab::Json.parse(error_message)['errorMessages']&.to_sentence
+ messages = Rails::Html::FullSanitizer.new.sanitize(messages).presence
+ return unless messages
+
+ s_('JiraRequest|An error occurred while requesting data from Jira: %{messages}. Check your Jira integration configuration and try again.') % { messages: messages }
+ rescue JSON::ParserError
+ end
end
def url
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 5d3c4a5c54a..3e809b11024 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -80,6 +80,11 @@ module Members
def after_execute(member:)
super
+ track_invite_source(member)
+ track_areas_of_focus(member)
+ end
+
+ def track_invite_source(member)
Gitlab::Tracking.event(self.class.name, 'create_member', label: invite_source, property: tracking_property(member), user: current_user)
end
@@ -94,6 +99,16 @@ module Members
member.invite? ? 'net_new_user' : 'existing_user'
end
+ def track_areas_of_focus(member)
+ areas_of_focus.each do |area_of_focus|
+ Gitlab::Tracking.event(self.class.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s)
+ end
+ end
+
+ def areas_of_focus
+ params[:areas_of_focus] || []
+ end
+
def user_limit
limit = params.fetch(:limit, DEFAULT_INVITE_LIMIT)
diff --git a/app/services/members/import_project_team_service.rb b/app/services/members/import_project_team_service.rb
new file mode 100644
index 00000000000..5f4d5414cfa
--- /dev/null
+++ b/app/services/members/import_project_team_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Members
+ class ImportProjectTeamService < BaseService
+ attr_reader :params, :current_user
+
+ def target_project_id
+ @target_project_id ||= params[:id].presence
+ end
+
+ def source_project_id
+ @source_project_id ||= params[:project_id].presence
+ end
+
+ def target_project
+ @target_project ||= Project.find_by_id(target_project_id)
+ end
+
+ def source_project
+ @source_project ||= Project.find_by_id(source_project_id)
+ end
+
+ def execute
+ import_project_team
+ end
+
+ private
+
+ def import_project_team
+ return false unless target_project.present? && source_project.present? && current_user.present?
+ return false unless can?(current_user, :read_project_member, source_project)
+ return false unless can?(current_user, :admin_project_member, target_project)
+
+ target_project.team.import(source_project, current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 099ab1d26e9..0a652c58aab 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -61,8 +61,8 @@ module MergeRequests
end
def cleanup_environments(merge_request)
- Ci::StopEnvironmentsService.new(merge_request.source_project, current_user)
- .execute_for_merge_request(merge_request)
+ Environments::StopService.new(merge_request.source_project, current_user)
+ .execute_for_merge_request(merge_request)
end
def cancel_review_app_jobs!(merge_request)
diff --git a/app/services/merge_requests/conflicts/base_service.rb b/app/services/merge_requests/conflicts/base_service.rb
index 402f6c4e4c0..70add2fdcb1 100644
--- a/app/services/merge_requests/conflicts/base_service.rb
+++ b/app/services/merge_requests/conflicts/base_service.rb
@@ -3,10 +3,11 @@
module MergeRequests
module Conflicts
class BaseService
- attr_reader :merge_request
+ attr_reader :merge_request, :params
- def initialize(merge_request)
+ def initialize(merge_request, params = {})
@merge_request = merge_request
+ @params = params
end
end
end
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
index 30a493e91ce..575a6bfe95a 100644
--- a/app/services/merge_requests/conflicts/list_service.rb
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -23,7 +23,11 @@ module MergeRequests
end
def conflicts
- @conflicts ||= Gitlab::Conflict::FileCollection.new(merge_request)
+ @conflicts ||=
+ Gitlab::Conflict::FileCollection.new(
+ merge_request,
+ allow_tree_conflicts: params[:allow_tree_conflicts]
+ )
end
end
end
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index ebeba0ee5b8..6b032545230 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -3,7 +3,7 @@
module MergeRequests
class CreatePipelineService < MergeRequests::BaseService
def execute(merge_request)
- return unless can_create_pipeline_for?(merge_request)
+ return cannot_create_pipeline_error unless can_create_pipeline_for?(merge_request)
create_detached_merge_request_pipeline(merge_request)
end
@@ -60,6 +60,10 @@ module MergeRequests
::Gitlab::UserAccess.new(current_user, container: merge_request.target_project)
.can_update_branch?(merge_request.source_branch_ref)
end
+
+ def cannot_create_pipeline_error
+ ServiceResponse.error(message: 'Cannot create a pipeline for this merge request.', payload: nil)
+ end
end
end
diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb
index e07e0c985b4..eda652c4b9a 100644
--- a/app/services/merge_requests/merge_to_ref_service.rb
+++ b/app/services/merge_requests/merge_to_ref_service.rb
@@ -66,6 +66,16 @@ module MergeRequests
end
def commit
+ if Feature.enabled?(:cache_merge_to_ref_calls, project, default_enabled: false)
+ Rails.cache.fetch(cache_key, expires_in: 1.day) do
+ extracted_merge_to_ref
+ end
+ else
+ extracted_merge_to_ref
+ end
+ end
+
+ def extracted_merge_to_ref
repository.merge_to_ref(current_user,
source_sha: source,
branch: merge_request.target_branch,
@@ -76,5 +86,9 @@ module MergeRequests
rescue Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError => error
raise MergeError, error.message
end
+
+ def cache_key
+ [:merge_to_ref_service, project.full_path, merge_request.target_branch_sha, merge_request.source_branch_sha]
+ end
end
end
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index 79b7eb8d9d8..adbe3ddfdad 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -65,7 +65,7 @@ module MergeRequests
end
if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target])
- errors << "Branch #{push_options[:target]} does not exist"
+ errors << "Target branch #{target_project.full_path}:#{push_options[:target]} does not exist"
end
end
diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb
index f7f0cf9abe8..0401653cf3c 100644
--- a/app/services/namespaces/in_product_marketing_emails_service.rb
+++ b/app/services/namespaces/in_product_marketing_emails_service.rb
@@ -8,8 +8,23 @@ module Namespaces
completed_actions: [:created],
incomplete_actions: [:git_write]
},
+ team_short: {
+ interval_days: [1],
+ completed_actions: [:git_write],
+ incomplete_actions: [:user_added]
+ },
+ trial_short: {
+ interval_days: [2],
+ completed_actions: [:git_write],
+ incomplete_actions: [:trial_started]
+ },
+ admin_verify: {
+ interval_days: [3],
+ completed_actions: [:git_write],
+ incomplete_actions: [:pipeline_created]
+ },
verify: {
- interval_days: [1, 5, 10],
+ interval_days: [4, 8, 13],
completed_actions: [:git_write],
incomplete_actions: [:pipeline_created]
},
@@ -98,14 +113,14 @@ module Namespaces
def can_perform_action?(user, group)
case track
- when :create
- user.can?(:create_projects, group)
- when :verify
+ when :create, :verify
user.can?(:create_projects, group)
- when :trial
+ when :trial, :trial_short
user.can?(:start_trial, group)
- when :team
+ when :team, :team_short
user.can?(:admin_group_member, group)
+ when :admin_verify
+ user.can?(:admin_group, group)
when :experience
true
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 542fafb901b..194c3d7bf7b 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -4,7 +4,7 @@ module Notes
class CreateService < ::Notes::BaseService
include IncidentManagement::UsageData
- def execute
+ def execute(skip_capture_diff_note_position: false)
note = Notes::BuildService.new(project, current_user, params.except(:merge_request_diff_head_sha)).execute
# n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440
@@ -34,7 +34,7 @@ module Notes
end
end
- when_saved(note) if note_saved
+ when_saved(note, skip_capture_diff_note_position: skip_capture_diff_note_position) if note_saved
end
note
@@ -68,14 +68,14 @@ module Notes
end
end
- def when_saved(note)
+ def when_saved(note, skip_capture_diff_note_position: false)
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
increment_usage_counter(note)
track_event(note, current_user)
- if note.for_merge_request? && note.diff_note? && note.start_of_discussion?
+ if !skip_capture_diff_note_position && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
end
diff --git a/app/services/packages/composer/create_package_service.rb b/app/services/packages/composer/create_package_service.rb
index c84d40c3753..8215a3385a4 100644
--- a/app/services/packages/composer/create_package_service.rb
+++ b/app/services/packages/composer/create_package_service.rb
@@ -17,7 +17,9 @@ module Packages
})
end
- ::Packages::Composer::CacheUpdateWorker.perform_async(created_package.project_id, created_package.name, nil)
+ unless Feature.enabled?(:remove_composer_v1_cache_code, project)
+ ::Packages::Composer::CacheUpdateWorker.perform_async(created_package.project_id, created_package.name, nil)
+ end
created_package
end
diff --git a/app/services/packages/create_dependency_service.rb b/app/services/packages/create_dependency_service.rb
index 2999885d55d..2c80ec66dbc 100644
--- a/app/services/packages/create_dependency_service.rb
+++ b/app/services/packages/create_dependency_service.rb
@@ -27,7 +27,7 @@ module Packages
dependencies_to_insert = names_and_version_patterns.reject { |k, _| k.in?(existing_names) }
end
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
inserted_ids = bulk_insert_package_dependencies(dependencies_to_insert)
bulk_insert_package_dependency_links(type, (existing_ids + inserted_ids))
end
@@ -76,7 +76,7 @@ module Packages
end
def database
- ::Gitlab::Database
+ ::Gitlab::Database.main
end
end
end
diff --git a/app/services/packages/debian/generate_distribution_key_service.rb b/app/services/packages/debian/generate_distribution_key_service.rb
index 28c97c7681e..917965da58e 100644
--- a/app/services/packages/debian/generate_distribution_key_service.rb
+++ b/app/services/packages/debian/generate_distribution_key_service.rb
@@ -5,20 +5,42 @@ module Packages
class GenerateDistributionKeyService
include Gitlab::Utils::StrongMemoize
- def initialize(current_user:, params: {})
- @current_user = current_user
+ def initialize(params: {})
@params = params
end
def execute
- raise ArgumentError, 'Please provide a user' unless current_user.is_a?(User)
+ 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
- generate_key
+ # 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
private
- attr_reader :current_user, :params
+ attr_reader :params
def passphrase
strong_memoize(:passphrase) do
@@ -72,35 +94,6 @@ module Packages
}.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
index 651325c49a0..74b07e05aa6 100644
--- a/app/services/packages/debian/generate_distribution_service.rb
+++ b/app/services/packages/debian/generate_distribution_service.rb
@@ -12,7 +12,7 @@ module Packages
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(
+ METADATA_KEYS = %w(
Package
Source
Binary
@@ -89,15 +89,18 @@ module Packages
@distribution.components.ordered_by_name.each do |component|
@distribution.architectures.ordered_by_name.each do |architecture|
generate_component_file(component, :packages, architecture, :deb)
+ generate_component_file(component, :di_packages, architecture, :udeb)
end
+ generate_component_file(component, :source, nil, :dsc)
end
end
def generate_component_file(component, component_file_type, architecture, package_file_type)
paragraphs = @distribution.package_files
+ .preload_package
.preload_debian_file_metadata
.with_debian_component_name(component.name)
- .with_debian_architecture_name(architecture.name)
+ .with_debian_architecture_name(architecture&.name)
.with_debian_file_type(package_file_type)
.find_each
.map(&method(:package_stanza_from_fields))
@@ -106,21 +109,49 @@ module Packages
def package_stanza_from_fields(package_file)
[
- BINARIES_METADATA.map do |metadata_key|
- rfc822_field(metadata_key, package_file.debian_fields[metadata_key])
+ METADATA_KEYS.map do |metadata_key|
+ metadata_name = metadata_key
+ metadata_value = package_file.debian_fields[metadata_key]
+
+ if package_file.debian_dsc?
+ metadata_name = 'Package' if metadata_key == 'Source'
+ checksum = case metadata_key
+ when 'Files' then package_file.file_md5
+ when 'Checksums-Sha256' then package_file.file_sha256
+ when 'Checksums-Sha1' then package_file.file_sha1
+ end
+
+ if checksum
+ metadata_value = "\n#{checksum} #{package_file.size} #{package_file.file_name}#{metadata_value}"
+ end
+ end
+
+ rfc822_field(metadata_name, metadata_value)
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)
+ package_file_extra_fields(package_file)
].flatten.compact.join('')
end
- def package_filename(package_file)
+ def package_file_extra_fields(package_file)
+ if package_file.debian_dsc?
+ [
+ rfc822_field('Directory', package_dirname(package_file))
+ ]
+ else
+ [
+ rfc822_field('Filename', "#{package_dirname(package_file)}/#{package_file.file_name}"),
+ rfc822_field('Size', package_file.size),
+ rfc822_field('MD5sum', package_file.file_md5),
+ rfc822_field('SHA256', package_file.file_sha256)
+ ]
+ end
+ end
+
+ def package_dirname(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}"
+ "#{pool_prefix(package_file)}/#{letter}/#{package_file.package.name}/#{package_file.package.version}"
end
def pool_prefix(package_file)
@@ -128,7 +159,7 @@ module Packages
when ::Packages::Debian::GroupDistribution
"pool/#{@distribution.codename}/#{package_file.package.project_id}"
else
- "pool/#{@distribution.codename}/#{@distribution.container_id}"
+ "pool/#{@distribution.codename}"
end
end
@@ -161,28 +192,37 @@ module Packages
end
def generate_release
- @distribution.file = CarrierWaveStringFile.new(release_header + release_sums)
+ @distribution.key || @distribution.create_key(GenerateDistributionKeyService.new.execute)
+ @distribution.file = CarrierWaveStringFile.new(release_content)
+ @distribution.file_signature = SignDistributionService.new(@distribution, release_content, detach: true).execute
+ @distribution.signed_file = CarrierWaveStringFile.new(
+ SignDistributionService.new(@distribution, release_content).execute
+ )
@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('')
+ def release_content
+ strong_memoize(:release_content) do
+ release_header + release_sums
end
end
+ def release_header
+ [
+ %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
+
def release_date
strong_memoize(:release_date) do
Time.now.utc
@@ -197,7 +237,8 @@ module Packages
return unless condition
return if value.blank?
- "#{name}: #{value.to_s.gsub("\n\n", "\n.\n").gsub("\n", "\n ")}\n"
+ value = " #{value}" unless value[0] == "\n"
+ "#{name}:#{value.to_s.gsub("\n\n", "\n.\n").gsub("\n", "\n ")}\n"
end
def valid_until_field
diff --git a/app/services/packages/debian/sign_distribution_service.rb b/app/services/packages/debian/sign_distribution_service.rb
new file mode 100644
index 00000000000..7797f7e9c0a
--- /dev/null
+++ b/app/services/packages/debian/sign_distribution_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class SignDistributionService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(distribution, content, detach: false)
+ @distribution = distribution
+ @content = content
+ @detach = detach
+ end
+
+ def execute
+ raise ArgumentError, 'distribution key is missing' unless @distribution.key
+
+ sig_mode = GPGME::GPGME_SIG_MODE_CLEAR
+
+ sig_mode = GPGME::GPGME_SIG_MODE_DETACH if @detach
+
+ Gitlab::Gpg.using_tmp_keychain do
+ GPGME::Ctx.new(
+ armor: true,
+ offline: true,
+ pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK,
+ password: @distribution.key.passphrase
+ ) do |ctx|
+ ctx.import(GPGME::Data.from_str(@distribution.key.public_key))
+ ctx.import(GPGME::Data.from_str(@distribution.key.private_key))
+ signature = GPGME::Data.new
+ ctx.sign(GPGME::Data.from_str(@content), signature, sig_mode)
+ signature.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/go/create_package_service.rb b/app/services/packages/go/create_package_service.rb
index 4e8b8ef8d6b..2a6eeff402e 100644
--- a/app/services/packages/go/create_package_service.rb
+++ b/app/services/packages/go/create_package_service.rb
@@ -23,7 +23,7 @@ module Packages
files[:mod] = prepare_file(version, :mod, version.gomod)
files[:zip] = prepare_file(version, :zip, version.archive.string)
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
# create new package and files
package = create_package
files.each { |type, (file, digests)| create_file(package, type, file, digests) }
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index 22396eb7687..1d5d9c38432 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -9,7 +9,7 @@ module Packages
return error('Package already exists.', 403) if current_package_exists?
return error('File is too large.', 400) if file_size_exceeded?
- ActiveRecord::Base.transaction { create_npm_package! }
+ ApplicationRecord.transaction { create_npm_package! }
end
private
diff --git a/app/services/packages/nuget/create_dependency_service.rb b/app/services/packages/nuget/create_dependency_service.rb
index 62ab485c0fc..3fc42056d43 100644
--- a/app/services/packages/nuget/create_dependency_service.rb
+++ b/app/services/packages/nuget/create_dependency_service.rb
@@ -41,7 +41,7 @@ module Packages
}
end
- ::Gitlab::Database.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert(::Packages::Nuget::DependencyLinkMetadatum.table_name, rows.compact) # rubocop:disable Gitlab/BulkInsert
end
def raw_dependency_for(dependency)
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 2d1733421fd..6ffe4f097f4 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -21,22 +21,11 @@ module Packages
try_obtain_lease do
@package_file.transaction do
- if existing_package
- package = link_to_existing_package
- elsif symbol_package?
- raise InvalidMetadataError, 'symbol package is invalid, matching package does not exist'
+ if use_new_package_file_updater?
+ new_execute
else
- package = update_linked_package
+ legacy_execute
end
-
- update_package(package)
-
- # Updating file_name updates the path where the file is stored.
- # We must pass the file again so that CarrierWave can handle the update
- @package_file.update!(
- file_name: package_filename,
- file: @package_file.file
- )
end
end
rescue ActiveRecord::RecordInvalid => e
@@ -45,6 +34,52 @@ module Packages
private
+ def new_execute
+ package_to_destroy = nil
+ target_package = @package_file.package
+
+ if existing_package
+ package_to_destroy = @package_file.package
+ target_package = existing_package
+ else
+ if symbol_package?
+ raise InvalidMetadataError, 'symbol package is invalid, matching package does not exist'
+ end
+
+ update_linked_package
+ end
+
+ update_package(target_package)
+
+ ::Packages::UpdatePackageFileService.new(@package_file, package_id: target_package.id, file_name: package_filename)
+ .execute
+
+ package_to_destroy&.destroy!
+ end
+
+ def legacy_execute
+ if existing_package
+ package = link_to_existing_package
+ elsif symbol_package?
+ raise InvalidMetadataError, 'symbol package is invalid, matching package does not exist'
+ else
+ package = update_linked_package
+ end
+
+ update_package(package)
+
+ # Updating file_name updates the path where the file is stored.
+ # We must pass the file again so that CarrierWave can handle the update
+ @package_file.update!(
+ file_name: package_filename,
+ file: @package_file.file
+ )
+ end
+
+ def use_new_package_file_updater?
+ ::Feature.enabled?(:packages_nuget_new_package_file_updater, @package_file.project, default_enabled: :yaml)
+ end
+
def update_package(package)
return if symbol_package?
diff --git a/app/services/packages/terraform_module/create_package_service.rb b/app/services/packages/terraform_module/create_package_service.rb
index fc376c70b00..03f749edfa8 100644
--- a/app/services/packages/terraform_module/create_package_service.rb
+++ b/app/services/packages/terraform_module/create_package_service.rb
@@ -11,7 +11,7 @@ module Packages
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! }
+ ApplicationRecord.transaction { create_terraform_module_package! }
end
private
diff --git a/app/services/packages/update_package_file_service.rb b/app/services/packages/update_package_file_service.rb
new file mode 100644
index 00000000000..9a8a78e509a
--- /dev/null
+++ b/app/services/packages/update_package_file_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Packages
+ class UpdatePackageFileService
+ delegate :file, to: :@package_file
+
+ def initialize(package_file, params)
+ @package_file = package_file
+ @params = params
+ end
+
+ def execute
+ check_params
+
+ return if same_as_params?
+
+ # we need to access the file *before* updating the attributes linked to its path/key.
+ file_storage_mode = file.file_storage?
+
+ @package_file.package_id = package_id if package_id
+ @package_file.file_name = file_name if file_name
+
+ if file_storage_mode
+ # package file is in mode LOCAL: we can pass the `file` to the update
+ @package_file.file = file
+ else
+ # package file is in mode REMOTE: don't pass the `file` to the update
+ # instead, pass the new file path. This will move the file
+ # in object storage.
+ @package_file.new_file_path = File.join(file.store_dir, @package_file.file_name)
+ end
+
+ @package_file.save!
+ end
+
+ private
+
+ def check_params
+ raise ArgumentError, 'package_file not persisted' unless @package_file.persisted?
+ raise ArgumentError, 'package_id and file_name are blank' if package_id.blank? && file_name.blank?
+ end
+
+ def same_as_params?
+ return false if package_id && package_id != @package_file.package_id
+ return false if file_name && file_name != @package_file.file_name
+
+ true
+ end
+
+ def package_id
+ @params[:package_id]
+ end
+
+ def file_name
+ @params[:file_name]
+ end
+ end
+end
diff --git a/app/services/packages/update_tags_service.rb b/app/services/packages/update_tags_service.rb
index adfad52910e..2bdf75a6617 100644
--- a/app/services/packages/update_tags_service.rb
+++ b/app/services/packages/update_tags_service.rb
@@ -15,7 +15,7 @@ module Packages
tags_to_create = @tags - existing_tags
@package.tags.with_name(tags_to_destroy).delete_all if tags_to_destroy.any?
- ::Gitlab::Database.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
+ ::Gitlab::Database.main.bulk_insert(Packages::Tag.table_name, rows(tags_to_create)) if tags_to_create.any? # rubocop:disable Gitlab/BulkInsert
end
private
diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb
index a6d49f03c0b..f5638b0aa40 100644
--- a/app/services/post_receive_service.rb
+++ b/app/services/post_receive_service.rb
@@ -17,13 +17,18 @@ class PostReceiveService
response = Gitlab::InternalPostReceive::Response.new
push_options = Gitlab::PushOptions.new(params[:push_options])
+ mr_options = push_options.get(:merge_request)
response.reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
+ # The PostReceive worker will normally invalidate the cache. However, it
+ # runs asynchronously. If push options require us to create a new merge
+ # request synchronously, we can't rely on that, so invalidate the cache here
+ repository&.expire_branches_cache if mr_options&.fetch(:create, false)
+
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes], push_options.as_json)
- mr_options = push_options.get(:merge_request)
if mr_options.present?
message = process_mr_push_options(mr_options, params[:changes])
response.add_alert_message(message)
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index 6d389035922..953b386b754 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -83,7 +83,7 @@ module Projects
def update_repository_configuration
project.reload_repository!
- project.write_repository_config
+ project.set_full_path
project.track_project_repository
end
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 5eafa5f9b29..75be3425029 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -65,7 +65,7 @@ module Projects
def cleanup_diffs(response)
old_commit_shas = extract_old_commit_shas(response.entries)
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
cleanup_merge_request_diffs(old_commit_shas)
cleanup_note_diff_files(old_commit_shas)
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 9a5c260e488..302c047a65f 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -65,7 +65,7 @@ module Projects
save_project_and_import_data
- Gitlab::ApplicationContext.with_context(related_class: "Projects::CreateService", project: @project) do
+ Gitlab::ApplicationContext.with_context(project: @project) do
after_create_actions if @project.persisted?
import_schedule
@@ -92,7 +92,7 @@ module Projects
# Skip writing the config for project imports/forks because it
# will always fail since the Git directory doesn't exist until
# a background job creates it (see Project#add_import_job).
- @project.write_repository_config unless @project.import?
+ @project.set_full_path unless @project.import?
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
@@ -101,6 +101,8 @@ module Projects
@project.track_project_repository
@project.create_project_setting unless @project.project_setting
+ yield if block_given?
+
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
@@ -162,7 +164,7 @@ module Projects
@project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save
- Integration.create_from_active_default_integrations(@project, :project_id, with_templates: true)
+ Integration.create_from_active_default_integrations(@project, :project_id)
@project.create_labels unless @project.gitlab_project_import?
diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb
index c57773c3302..0356a6b0ccd 100644
--- a/app/services/projects/detect_repository_languages_service.rb
+++ b/app/services/projects/detect_repository_languages_service.rb
@@ -21,7 +21,7 @@ module Projects
.update_all(share: update[:share])
end
- Gitlab::Database.bulk_insert( # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert( # rubocop:disable Gitlab/BulkInsert
RepositoryLanguage.table_name,
detection.insertions(matching_programming_languages)
)
diff --git a/app/services/projects/fetch_statistics_increment_service.rb b/app/services/projects/fetch_statistics_increment_service.rb
index b150fd2d9f1..3354a074d1e 100644
--- a/app/services/projects/fetch_statistics_increment_service.rb
+++ b/app/services/projects/fetch_statistics_increment_service.rb
@@ -15,7 +15,7 @@ module Projects
ON CONFLICT (project_id, date) DO UPDATE SET fetch_count = #{table_name}.fetch_count + 1
SQL
- ActiveRecord::Base.connection.execute(increment_fetch_count_sql)
+ ProjectDailyStatistic.connection.execute(increment_fetch_count_sql)
end
private
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index c7989e04607..b65d0e63fd3 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -14,7 +14,7 @@ module Projects
result = move_repositories
if result
- project.write_repository_config
+ project.set_full_path
project.track_project_repository
else
rollback_folder_move
diff --git a/app/services/projects/hashed_storage/rollback_repository_service.rb b/app/services/projects/hashed_storage/rollback_repository_service.rb
index 6ab49630603..f4146ff9158 100644
--- a/app/services/projects/hashed_storage/rollback_repository_service.rb
+++ b/app/services/projects/hashed_storage/rollback_repository_service.rb
@@ -14,7 +14,7 @@ module Projects
result = move_repositories
if result
- project.write_repository_config
+ project.set_full_path
project.track_project_repository
else
rollback_folder_move
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index e86106f0a09..7c00b9e6105 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -38,7 +38,7 @@ module Projects
rows = existent_lfs_objects
.not_linked_to_project(project)
.map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } }
- Gitlab::Database.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(:lfs_objects_projects, rows) # rubocop:disable Gitlab/BulkInsert
iterations += 1
linked_existing_objects += existent_lfs_objects.map(&:oid)
diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
index 75106297043..b4872cd9442 100644
--- a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
@@ -31,7 +31,7 @@ module Projects
#
LfsDownloadLinkListService
.new(project, remote_uri: current_endpoint_uri)
- .execute(lfs_pointers_in_repository)
+ .execute(missing_lfs_files)
rescue LfsDownloadLinkListService::DownloadLinksError => e
raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
end
@@ -53,6 +53,22 @@ module Projects
@lfs_pointers_in_repository ||= LfsListService.new(project).execute
end
+ def existing_lfs_objects
+ project.lfs_objects
+ end
+
+ def existing_lfs_objects_hash
+ {}.tap do |hash|
+ existing_lfs_objects.find_each do |lfs_object|
+ hash[lfs_object.oid] = lfs_object.size
+ end
+ end
+ end
+
+ def missing_lfs_files
+ lfs_pointers_in_repository.except(*existing_lfs_objects_hash.keys)
+ end
+
def lfsconfig_endpoint_uri
strong_memoize(:lfsconfig_endpoint_uri) do
# Retrieveing the blob data from the .lfsconfig file
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index 2cc6bcdf57c..51b8e3c6c54 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -63,8 +63,15 @@ module Projects
# Make sure we're converting to symbols because
# * ActionController::Parameters#keys returns a list of strings
# * in specs we're using hashes with symbols as keys
+ update_keys = settings.keys.map(&:to_sym)
- settings.keys.map(&:to_sym) == %i[enabled]
+ # Integrated error tracking works without Sentry integration,
+ # so we don't need to update all those values from error_tracking_params_for_update method.
+ # Instead we turn it on/off with partial update together with "enabled" attribute.
+ # But since its optional, we exclude it from the condition below.
+ update_keys.delete(:integrated)
+
+ update_keys == %i[enabled]
end
def error_tracking_params_for_partial_update(settings)
diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb
index 6be3b1b5a6f..f35370c427f 100644
--- a/app/services/projects/overwrite_project_service.rb
+++ b/app/services/projects/overwrite_project_service.rb
@@ -5,6 +5,8 @@ module Projects
def execute(source_project)
return unless source_project && source_project.namespace == @project.namespace
+ start_time = ::Gitlab::Metrics::System.monotonic_time
+
Project.transaction do
move_before_destroy_relationships(source_project)
# Reset is required in order to get the proper
@@ -25,10 +27,25 @@ module Projects
else
raise
end
+
+ ensure
+ track_service(start_time, source_project, e)
end
private
+ def track_service(start_time, source_project, exception)
+ return if ::Feature.disabled?(:project_overwrite_service_tracking, source_project, default_enabled: :yaml)
+
+ duration = ::Gitlab::Metrics::System.monotonic_time - start_time
+
+ Gitlab::AppJsonLogger.info(class: self.class.name,
+ namespace_id: source_project.namespace.id,
+ project_id: source_project.id,
+ duration_s: duration.to_f,
+ error: exception.class.name)
+ end
+
def move_before_destroy_relationships(source_project)
options = { remove_remaining_elements: false }
diff --git a/app/services/projects/protect_default_branch_service.rb b/app/services/projects/protect_default_branch_service.rb
index 0111b9e377a..03d1c49657d 100644
--- a/app/services/projects/protect_default_branch_service.rb
+++ b/app/services/projects/protect_default_branch_service.rb
@@ -69,3 +69,5 @@ module Projects
end
end
end
+
+Projects::ProtectDefaultBranchService.prepend_mod
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index fb0fea756bc..074550e104d 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -89,6 +89,8 @@ module Projects
update_integrations
+ remove_paid_features
+
project.old_path_with_namespace = @old_path
update_repository_configuration(@new_path)
@@ -109,6 +111,10 @@ module Projects
move_pages(project)
end
+ # Overridden in EE
+ def remove_paid_features
+ end
+
def transfer_missing_group_resources(group)
Labels::TransferService.new(current_user, group, project).execute
@@ -129,7 +135,7 @@ module Projects
end
def update_repository_configuration(full_path)
- project.write_repository_config(gl_full_path: full_path)
+ project.set_full_path(gl_full_path: full_path)
project.track_project_repository
end
@@ -235,7 +241,7 @@ module Projects
end
def update_integrations
- project.integrations.inherit.delete_all
+ project.integrations.with_default_settings.delete_all
Integration.create_from_active_default_integrations(project, :project_id)
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index a90c22c7de5..f96a6ee1255 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -37,14 +37,13 @@ module Projects
job.run!
end
- raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
- raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?
+ validate_state!
+ validate_max_size!
+ validate_max_entries!
build.artifacts_file.use_file do |artifacts_path|
deploy_to_legacy_storage(artifacts_path)
-
create_pages_deployment(artifacts_path, build)
-
success
end
rescue InvalidStateError => e
@@ -92,8 +91,10 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(tmp_path, PUBLIC_DIR)
+
raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
- raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?
+
+ validate_outdated_sha!
deploy_page!(archive_public_path)
end
@@ -108,15 +109,6 @@ module Projects
end
def extract_zip_archive!(artifacts_path, temp_path)
- raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
-
- # Calculate page size after extract
- public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true)
-
- if public_entry.total_size > max_size
- raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
- end
-
SafeZip::Extract.new(artifacts_path)
.extract(directories: [PUBLIC_DIR], to: temp_path)
rescue SafeZip::Extract::Error => e
@@ -151,19 +143,17 @@ module Projects
end
def create_pages_deployment(artifacts_path, build)
- # we're using the full archive and pages daemon needs to read it
- # so we want the total count from entries, not only "public/" directory
- # because it better approximates work we need to do before we can serve the site
- entries_count = build.artifacts_metadata_entry("", recursive: true).entries.count
sha256 = build.job_artifacts_archive.file_sha256
deployment = nil
File.open(artifacts_path) do |file|
deployment = project.pages_deployments.create!(file: file,
file_count: entries_count,
- file_sha256: sha256)
+ file_sha256: sha256,
+ ci_build_id: build.id
+ )
- raise InvalidStateError, 'build SHA is outdated for this ref' unless latest?
+ validate_outdated_sha!
project.update_pages_deployment!(deployment)
end
@@ -175,29 +165,6 @@ module Projects
)
end
- def latest?
- # check if sha for the ref is still the most recent one
- # this helps in case when multiple deployments happens
- sha == latest_sha
- end
-
- def blocks
- # Calculate dd parameters: we limit the size of pages
- 1 + max_size / BLOCK_SIZE
- end
-
- def max_size_from_settings
- Gitlab::CurrentSettings.max_pages_size.megabytes
- end
-
- def max_size
- max_pages_size = max_size_from_settings
-
- return ::Gitlab::Pages::MAX_SIZE if max_pages_size == 0
-
- max_pages_size
- end
-
def tmp_path
@tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH)
end
@@ -262,6 +229,73 @@ module Projects
def tmp_dir_prefix
"project-#{project.id}-build-#{build.id}-"
end
+
+ def validate_state!
+ raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
+ raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
+
+ validate_outdated_sha!
+ end
+
+ def validate_outdated_sha!
+ return if latest?
+
+ if Feature.enabled?(:pages_smart_check_outdated_sha, project, default_enabled: :yaml)
+ # use pipeline_id in case the build is retried
+ last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
+
+ return unless last_deployed_pipeline_id
+ return if last_deployed_pipeline_id <= build.pipeline_id
+ end
+
+ raise InvalidStateError, 'build SHA is outdated for this ref'
+ end
+
+ def latest?
+ # check if sha for the ref is still the most recent one
+ # this helps in case when multiple deployments happens
+ sha == latest_sha
+ end
+
+ def validate_max_size!
+ if total_size > max_size
+ raise InvalidStateError, "artifacts for pages are too large: #{total_size}"
+ end
+ end
+
+ # Calculate page size after extract
+ def total_size
+ @total_size ||= build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true).total_size
+ end
+
+ def max_size_from_settings
+ Gitlab::CurrentSettings.max_pages_size.megabytes
+ end
+
+ def max_size
+ max_pages_size = max_size_from_settings
+
+ return ::Gitlab::Pages::MAX_SIZE if max_pages_size == 0
+
+ max_pages_size
+ end
+
+ def validate_max_entries!
+ if pages_file_entries_limit > 0 && entries_count > pages_file_entries_limit
+ raise InvalidStateError, "pages site contains #{entries_count} file entries, while limit is set to #{pages_file_entries_limit}"
+ end
+ end
+
+ def entries_count
+ # we're using the full archive and pages daemon needs to read it
+ # so we want the total count from entries, not only "public/" directory
+ # because it better approximates work we need to do before we can serve the site
+ @entries_count = build.artifacts_metadata_entry("", recursive: true).entries.count
+ end
+
+ def pages_file_entries_limit
+ project.actual_limits.pages_file_entries
+ end
end
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 6c29ba81910..898125c181c 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -43,12 +43,7 @@ module Projects
# LFS objects must be sent first, or the push has dangling pointers
send_lfs_objects!(remote_mirror)
- response = if Feature.enabled?(:update_remote_mirror_inmemory, project, default_enabled: :yaml)
- remote_mirror.update_repository(inmemory_remote: true)
- else
- remote_mirror.ensure_remote!
- remote_mirror.update_repository(inmemory_remote: false)
- end
+ response = remote_mirror.update_repository
if response.divergent_refs.any?
message = "Some refs have diverged and have not been updated on the remote:"
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index b4b493624e7..249333e6d13 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -83,15 +83,6 @@ module Releases
release.execute_hooks(action)
end
- def track_protected_tag_access_error!
- unless ::Gitlab::UserAccess.new(current_user, container: project).can_create_tag?(tag_name)
- Gitlab::ErrorTracking.log_exception(
- ReleaseProtectedTagAccessError.new,
- project_id: project.id,
- user_id: current_user.id)
- end
- end
-
# overridden in EE
def project_group_id; end
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 2aac5644b84..caa6a003205 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -7,8 +7,6 @@ module Releases
return error('Release already exists', 409) if release
return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
- track_protected_tag_access_error!
-
# should be found before the creation of new tag
# because tag creation can spawn new pipeline
# which won't have any data for evidence yet
@@ -48,8 +46,6 @@ module Releases
end
def can_create_tag?
- return true unless ::Feature.enabled?(:evalute_protected_tag_for_release_permissions, project, default_enabled: :yaml)
-
::Gitlab::UserAccess.new(current_user, container: project).can_create_tag?(tag_name)
end
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 36cf29c955d..8abf9308689 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -6,8 +6,6 @@ module Releases
return error('Release does not exist', 404) unless release
return error('Access Denied', 403) unless allowed?
- track_protected_tag_access_error!
-
if release.destroy
success(tag: existing_tag, release: release)
else
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index eda4b7102c0..2e0a2f8488a 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -7,8 +7,6 @@ module Releases
return error
end
- track_protected_tag_access_error!
-
if param_for_milestone_titles_provided?
previous_milestones = release.milestones.map(&:title)
params[:milestones] = milestones
@@ -18,7 +16,7 @@ module Releases
# when it does assign_attributes instead of actual saving
# this leads to the validation error being raised
# see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43385
- ActiveRecord::Base.transaction do
+ ApplicationRecord.transaction do
if release.update(params)
execute_hooks(release, 'update')
success(tag: existing_tag, release: release, milestones_updated: milestones_updated?(previous_milestones))
diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb
index 3797d41a5df..bc2d3a946cc 100644
--- a/app/services/resource_events/change_labels_service.rb
+++ b/app/services/resource_events/change_labels_service.rb
@@ -23,7 +23,7 @@ module ResourceEvents
label_hash.merge(label_id: label.id, action: ResourceLabelEvent.actions['remove'])
end
- Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(ResourceLabelEvent.table_name, labels) # rubocop:disable Gitlab/BulkInsert
resource.expire_note_etag_cache
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_label_changed_action(author: user) if resource.is_a?(Issue)
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 661aafc70cd..fe11820fb54 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -22,8 +22,9 @@ module Search
filters: { state: params[:state], confidential: params[:confidential] })
end
+ # rubocop: disable CodeReuse/ActiveRecord
def projects
- @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
+ @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute.includes(:topics, :taggings)
end
def allowed_scopes
diff --git a/app/services/security/merge_reports_service.rb b/app/services/security/merge_reports_service.rb
new file mode 100644
index 00000000000..5f6f98a3c39
--- /dev/null
+++ b/app/services/security/merge_reports_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Security
+ class MergeReportsService
+ attr_reader :source_reports
+
+ def initialize(*source_reports)
+ @source_reports = source_reports
+ end
+
+ def execute
+ copy_resources_to_target_report
+ copy_findings_to_target
+ target_report
+ end
+
+ private
+
+ def target_report
+ @target_report ||= ::Gitlab::Ci::Reports::Security::Report.new(
+ source_reports.first.type,
+ source_reports.first.pipeline,
+ source_reports.first.created_at
+ ).tap { |report| report.errors = source_reports.flat_map(&:errors) }
+ end
+
+ def copy_resources_to_target_report
+ sorted_source_reports.each do |source_report|
+ copy_scanners_to_target(source_report)
+ copy_identifiers_to_target(source_report)
+ copy_scanned_resources_to_target(source_report)
+ end
+ end
+
+ def sorted_source_reports
+ source_reports.sort { |a, b| a.primary_scanner_order_to(b) }
+ end
+
+ def copy_scanners_to_target(source_report)
+ # no need for de-duping: it's done by Report internally
+ source_report.scanners.values.each { |scanner| target_report.add_scanner(scanner) }
+ end
+
+ def copy_identifiers_to_target(source_report)
+ # no need for de-duping: it's done by Report internally
+ source_report.identifiers.values.each { |identifier| target_report.add_identifier(identifier) }
+ end
+
+ def copy_scanned_resources_to_target(source_report)
+ target_report.scanned_resources.concat(source_report.scanned_resources).uniq!
+ end
+
+ def copy_findings_to_target
+ deduplicated_findings.sort.each { |finding| target_report.add_finding(finding) }
+ end
+
+ def deduplicated_findings
+ prioritized_findings.each_with_object([[], Set.new]) do |finding, (deduplicated, seen_identifiers)|
+ next if seen_identifiers.intersect?(finding.keys.to_set)
+
+ seen_identifiers.merge(finding.keys)
+ deduplicated << finding
+ end.first
+ end
+
+ def prioritized_findings
+ source_reports.flat_map(&:findings).sort { |a, b| a.scanner_order_to(b) }
+ end
+ end
+end
diff --git a/app/services/service_ping/permit_data_categories_service.rb b/app/services/service_ping/permit_data_categories_service.rb
index ff48c022b56..d8fa255a485 100644
--- a/app/services/service_ping/permit_data_categories_service.rb
+++ b/app/services/service_ping/permit_data_categories_service.rb
@@ -2,10 +2,10 @@
module ServicePing
class PermitDataCategoriesService
- STANDARD_CATEGORY = 'Standard'
- SUBSCRIPTION_CATEGORY = 'Subscription'
- OPERATIONAL_CATEGORY = 'Operational'
- OPTIONAL_CATEGORY = 'Optional'
+ STANDARD_CATEGORY = 'standard'
+ SUBSCRIPTION_CATEGORY = 'subscription'
+ OPERATIONAL_CATEGORY = 'operational'
+ OPTIONAL_CATEGORY = 'optional'
CATEGORIES = [
STANDARD_CATEGORY,
SUBSCRIPTION_CATEGORY,
@@ -14,20 +14,10 @@ module ServicePing
].to_set.freeze
def execute
- return [] unless product_intelligence_enabled?
+ return [] unless ServicePingSettings.product_intelligence_enabled?
CATEGORIES
end
-
- def product_intelligence_enabled?
- pings_enabled? && !User.single_user&.requires_usage_stats_consent?
- end
-
- private
-
- def pings_enabled?
- ::Gitlab::CurrentSettings.usage_ping_enabled?
- end
end
end
diff --git a/app/services/service_ping/service_ping_settings.rb b/app/services/service_ping/service_ping_settings.rb
new file mode 100644
index 00000000000..6964210b1db
--- /dev/null
+++ b/app/services/service_ping/service_ping_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ServicePing
+ module ServicePingSettings
+ extend self
+
+ def product_intelligence_enabled?
+ enabled? && !User.single_user&.requires_usage_stats_consent?
+ end
+
+ def enabled?
+ ::Gitlab::CurrentSettings.usage_ping_enabled?
+ end
+ end
+end
+
+ServicePing::ServicePingSettings.extend_mod_with('ServicePing::ServicePingSettings')
diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb
index 5c03aa46e18..09d1670fd1f 100644
--- a/app/services/service_ping/submit_service.rb
+++ b/app/services/service_ping/submit_service.rb
@@ -18,7 +18,7 @@ module ServicePing
SubmissionError = Class.new(StandardError)
def execute
- return unless ServicePing::PermitDataCategoriesService.new.product_intelligence_enabled?
+ return unless ServicePing::ServicePingSettings.product_intelligence_enabled?
begin
usage_data = BuildPayloadService.new.execute
diff --git a/app/services/spam/mark_as_spam_service.rb b/app/services/spam/akismet_mark_as_spam_service.rb
index ed5e674d8e9..da5506b9a21 100644
--- a/app/services/spam/mark_as_spam_service.rb
+++ b/app/services/spam/akismet_mark_as_spam_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Spam
- class MarkAsSpamService
+ class AkismetMarkAsSpamService
include ::AkismetMethods
attr_accessor :target, :options
@@ -9,12 +9,12 @@ module Spam
def initialize(target:)
@target = target
@options = {}
+ end
+ def execute
@options[:ip_address] = @target.ip_address
@options[:user_agent] = @target.user_agent
- end
- def execute
return unless target.submittable_as_spam?
return unless akismet.submit_spam
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
index ec16ce19cf6..2a28b66f09b 100644
--- a/app/services/spam/spam_action_service.rb
+++ b/app/services/spam/spam_action_service.rb
@@ -28,7 +28,7 @@ module Spam
ServiceResponse.success(message: "CAPTCHA successfully verified")
else
return ServiceResponse.success(message: 'Skipped spam check because user was allowlisted') if allowlisted?(user)
- return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?
+ return ServiceResponse.success(message: 'Skipped spam check because it was not required') unless check_for_spam?(user: user)
perform_spam_service_check
ServiceResponse.success(message: "Spam check performed. Check #{target.class.name} spammable model for any errors or CAPTCHA requirement")
@@ -94,7 +94,7 @@ module Spam
def create_spam_log
@spam_log = SpamLog.create!(
{
- user_id: target.author_id,
+ user_id: user.id,
title: target.spam_title,
description: target.spam_description,
source_ip: spam_params.ip_address,
diff --git a/app/services/suggestions/create_service.rb b/app/services/suggestions/create_service.rb
index a97c36fa0ca..c5f9fa1eee0 100644
--- a/app/services/suggestions/create_service.rb
+++ b/app/services/suggestions/create_service.rb
@@ -25,7 +25,7 @@ module Suggestions
end
rows.in_groups_of(100, false) do |rows|
- Gitlab::Database.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert('suggestions', rows) # rubocop:disable Gitlab/BulkInsert
end
Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_add_suggestion_action(user: @note.author)
diff --git a/app/services/todos/destroy/destroyed_issuable_service.rb b/app/services/todos/destroy/destroyed_issuable_service.rb
index db12965224b..7a85b59eeea 100644
--- a/app/services/todos/destroy/destroyed_issuable_service.rb
+++ b/app/services/todos/destroy/destroyed_issuable_service.rb
@@ -20,7 +20,7 @@ module Todos
SQL
loop do
- result = ActiveRecord::Base.connection.execute(delete_query)
+ result = Todo.connection.execute(delete_query)
break if result.cmd_tuples == 0
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index dfe14225ade..1fe397d24e7 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -23,10 +23,10 @@ module Todos
return if user_has_reporter_access?
remove_confidential_resource_todos
+ remove_group_todos
if entity.private?
remove_project_todos
- remove_group_todos
else
enqueue_private_features_worker
end
@@ -68,7 +68,7 @@ module Todos
return unless entity.is_a?(Namespace)
Todo
- .for_group(non_authorized_non_public_groups)
+ .for_group(unauthorized_private_groups)
.for_user(user)
.delete_all
end
@@ -104,16 +104,13 @@ module Todos
GroupsFinder.new(user, min_access_level: Gitlab::Access::REPORTER).execute.select(:id)
end
- # since the entity is a private group, we can assume all subgroups are also
- # private. We can therefore limit GroupsFinder with `all_available: false`.
- # Otherwise it tries to include all public groups. This generates an expensive
- # SQL queries: https://gitlab.com/gitlab-org/gitlab/-/issues/325133
# rubocop: disable CodeReuse/ActiveRecord
- def non_authorized_non_public_groups
+ def unauthorized_private_groups
return [] unless entity.is_a?(Namespace)
- return [] unless entity.private?
- entity.self_and_descendants.select(:id)
+ groups = entity.self_and_descendants.private_only
+
+ groups.select(:id)
.id_not_in(GroupsFinder.new(user, all_available: false).execute.select(:id).reorder(nil))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/users/ban_service.rb b/app/services/users/ban_service.rb
index 247ed14966b..88e92ebff9b 100644
--- a/app/services/users/ban_service.rb
+++ b/app/services/users/ban_service.rb
@@ -1,25 +1,15 @@
# frozen_string_literal: true
module Users
- class BanService < BaseService
- def initialize(current_user)
- @current_user = current_user
- end
+ class BanService < BannedUserBaseService
+ private
- def execute(user)
- if user.ban
- log_event(user)
- success
- else
- messages = user.errors.full_messages
- error(messages.uniq.join('. '))
- end
+ def update_user(user)
+ user.ban
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}")
+ def action
+ :ban
end
end
end
diff --git a/app/services/users/banned_user_base_service.rb b/app/services/users/banned_user_base_service.rb
new file mode 100644
index 00000000000..16041075941
--- /dev/null
+++ b/app/services/users/banned_user_base_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Users
+ class BannedUserBaseService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ return permission_error unless allowed?
+
+ if update_user(user)
+ log_event(user)
+ success
+ else
+ messages = user.errors.full_messages
+ error(messages.uniq.join('. '))
+ end
+ end
+
+ private
+
+ attr_reader :current_user
+
+ def allowed?
+ can?(current_user, :admin_all_resources)
+ end
+
+ def permission_error
+ error(_("You are not allowed to %{action} a user" % { action: action.to_s }), :forbidden)
+ end
+
+ def log_event(user)
+ Gitlab::AppLogger.info(message: "User #{action}", user: "#{user.username}", email: "#{user.email}", "#{action}_by": "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ end
+ end
+end
diff --git a/app/services/users/set_status_service.rb b/app/services/users/set_status_service.rb
index 2b4be8c833b..d0bb40cbcfb 100644
--- a/app/services/users/set_status_service.rb
+++ b/app/services/users/set_status_service.rb
@@ -28,11 +28,12 @@ module Users
params[:emoji] = UserStatus::DEFAULT_EMOJI if params[:emoji].blank?
params[:availability] = UserStatus.availabilities[:not_set] unless new_user_availability
- user_status.update(params)
+ bump_user if user_status.update(params)
end
def remove_status
- UserStatus.delete(target_user.id)
+ bump_user if UserStatus.delete(target_user.id).nonzero?
+ true
end
def user_status
@@ -48,5 +49,12 @@ module Users
def new_user_availability
UserStatus.availabilities[params[:availability]]
end
+
+ def bump_user
+ # Intentionally not calling `touch` as that will trigger other callbacks
+ # on target_user (e.g. after_touch, after_commit, after_rollback) and we
+ # don't need them to happen here.
+ target_user.update_column(:updated_at, Time.current)
+ end
end
end
diff --git a/app/services/users/unban_service.rb b/app/services/users/unban_service.rb
new file mode 100644
index 00000000000..363783cf240
--- /dev/null
+++ b/app/services/users/unban_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Users
+ class UnbanService < BannedUserBaseService
+ private
+
+ def update_user(user)
+ user.activate
+ end
+
+ def action
+ :unban
+ end
+ end
+end
diff --git a/app/uploaders/packages/debian/distribution_release_file_uploader.rb b/app/uploaders/packages/debian/distribution_release_file_uploader.rb
index 9a30aac6396..a6ff3767b22 100644
--- a/app/uploaders/packages/debian/distribution_release_file_uploader.rb
+++ b/app/uploaders/packages/debian/distribution_release_file_uploader.rb
@@ -10,7 +10,12 @@ class Packages::Debian::DistributionReleaseFileUploader < GitlabUploader
alias_method :upload, :model
def filename
- 'Release'
+ case mounted_as
+ when :signed_file
+ 'InRelease'
+ else
+ 'Release'
+ end
end
def store_dir
diff --git a/app/validators/any_field_validator.rb b/app/validators/any_field_validator.rb
new file mode 100644
index 00000000000..b5d01c65585
--- /dev/null
+++ b/app/validators/any_field_validator.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# AnyFieldValidator
+#
+# Custom validator that checks if any of the provided
+# fields are present to ensure creation of a non-empty
+# record
+#
+# Example:
+#
+# class MyModel < ApplicationRecord
+# validates_with AnyFieldValidator, fields: %w[type name url]
+# end
+class AnyFieldValidator < ActiveModel::Validator
+ def initialize(*args)
+ super
+
+ if options[:fields].blank?
+ raise 'Provide the fields options'
+ end
+ end
+
+ def validate(record)
+ return unless one_of_required_fields.all? { |field| record[field].blank? }
+
+ record.errors.add(:base, _("At least one field of %{one_of_required_fields} must be present") %
+ { one_of_required_fields: one_of_required_fields })
+ end
+
+ private
+
+ def one_of_required_fields
+ options[:fields]
+ end
+end
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index fb530e18b03..ee97a678aaa 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -6,20 +6,24 @@
.form-check
= f.check_box :auto_devops_enabled, class: 'form-check-input'
= f.label :auto_devops_enabled, class: 'form-check-label' do
- %strong= s_('CICD|Default to Auto DevOps pipeline for all projects')
+ = s_('CICD|Default to Auto DevOps pipeline for all projects')
.form-text.text-muted
= s_('CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file.')
= link_to _('What is Auto DevOps?'), help_page_path('topics/autodevops/index.md'), target: '_blank'
.form-group
= f.label :auto_devops_domain, s_('AdminSettings|Auto DevOps domain'), class: 'label-bold'
- = f.text_field :auto_devops_domain, class: 'form-control gl-form-input', placeholder: 'domain.com'
+ = f.text_field :auto_devops_domain, class: 'form-control gl-form-input', placeholder: 'example.com'
.form-text.text-muted
- = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
+ = s_("AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects.")
+ = link_to _('Learn more.'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-review-apps'), target: '_blank'
+
.form-group
.form-check
= f.check_box :shared_runners_enabled, class: 'form-check-input'
= f.label :shared_runners_enabled, class: 'form-check-label' do
= s_("AdminSettings|Enable shared runners for new projects")
+ .form-text.text-muted
+ = s_("AdminSettings|All new projects can use the instance's shared runners by default.")
= render_if_exists 'admin/application_settings/shared_runners_minutes_setting', form: f
@@ -31,32 +35,32 @@
= f.label :max_artifacts_size, _('Maximum artifacts size (MB)'), class: 'label-bold'
= f.number_field :max_artifacts_size, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("Set the maximum file size for each job's artifacts")
- = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
+ = _("The maximum file size for job artifacts.")
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
.form-group
= f.label :default_artifacts_expire_in, _('Default artifacts expiration'), class: 'label-bold'
= f.text_field :default_artifacts_expire_in, class: 'form-control gl-form-input'
.form-text.text-muted
- = html_escape(_("Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: %{code_open}4 mins 2 sec%{code_close}, %{code_open}2h42min%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
+ = html_escape(_("The default expiration time for job artifacts. 0 for unlimited. The default unit is in seconds, but you can use other units, for example %{code_open}4 mins 2 sec%{code_close}, %{code_open}2h42min%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
.form-check
= f.check_box :keep_latest_artifact, class: 'form-check-input'
= f.label :keep_latest_artifact, class: 'form-check-label' do
- %strong
- = s_('AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines')
+ = s_('AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines')
.form-text.text-muted
= s_('AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire.')
.form-group
= f.label :archive_builds_in_human_readable, _('Archive jobs'), class: 'label-bold'
- = f.text_field :archive_builds_in_human_readable, class: 'form-control gl-form-input', placeholder: 'never'
+ = f.text_field :archive_builds_in_human_readable, class: 'form-control gl-form-input'
.form-text.text-muted
- = html_escape(_("Set the duration for which the jobs will be considered as old and expired. Once that time passes, the jobs will be archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: %{code_open}15 days%{code_close}, %{code_open}1 month%{code_close}, %{code_open}2 years%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = html_escape(_("Jobs older than the configured time are considered expired and are archived. Archived jobs can no longer be retried. Leave empty to never archive jobs automatically. The default unit is in days, but you can use other units, for example %{code_open}15 days%{code_close}, %{code_open}1 month%{code_close}, %{code_open}2 years%{code_close}. Minimum value is 1 day.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'archive-jobs')
.form-group
.form-check
= f.check_box :protected_ci_variables, class: 'form-check-input'
= f.label :protected_ci_variables, class: 'form-check-label' do
- %strong= s_('AdminSettings|Protect CI/CD variables by default')
+ = s_('AdminSettings|Protect CI/CD variables by default')
.form-text.text-muted
= s_('AdminSettings|New CI/CD variables in projects and groups default to protected.')
.form-group
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index b22aaabe41a..1c35250644d 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -8,20 +8,20 @@
= f.label :email_author_in_body, class: 'form-check-label' do
= _('Include author name in notification email body')
.form-text.text-muted
- = _('Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.')
+ = _("Include the name of the author of the issue, merge request or comment in the email body. By default, GitLab overrides the email sender's name. Some email servers don't support that option.")
.form-group
.form-check
= f.check_box :html_emails_enabled, class: 'form-check-input'
= f.label :html_emails_enabled, class: 'form-check-label' do
- = _('Enable HTML emails')
+ = _('Enable multipart emails')
.form-text.text-muted
- = _('By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.')
+ = _('Send email in multipart format (HTML and plain text). Uncheck to send email messages in plain text only.')
.form-group
= f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
= f.text_field :commit_email_hostname, class: 'form-control gl-form-input'
.form-text.text-muted
- commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email.md', anchor: 'custom-hostname-for-private-commit-emails'), target: '_blank'
- = _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
+ = _("Hostname used in private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= render_if_exists 'admin/application_settings/email_additional_text_setting', form: f
@@ -31,6 +31,6 @@
= f.label :in_product_marketing_emails_enabled, class: 'form-check-label' do
= _('Enable in-product marketing emails')
.form-text.text-muted
- = _('By default, GitLab sends emails to help guide users through the onboarding process.')
+ = _('Send emails to help guide new users through the onboarding process.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
index fd9e7ee50c4..70c1e3ce3c1 100644
--- a/app/views/admin/application_settings/_grafana.html.haml
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -2,16 +2,16 @@
= form_errors(@application_setting)
%fieldset
- %p
- = _("Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab.")
- = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/performance/grafana_configuration.md')
.form-group
.form-check
= f.check_box :grafana_enabled, class: 'form-check-input'
= f.label :grafana_enabled, class: 'form-check-label' do
- = _('Enable access to Grafana')
+ = _("Add a link to Grafana")
+ .form-text.text-muted
+ = _("A Metrics Dashboard menu item appears in the Monitoring section of the Admin Area.")
.form-group
= f.label :grafana_url, _('Grafana URL'), class: 'label-bold'
= f.text_field :grafana_url, class: 'form-control gl-form-input', placeholder: '/-/grafana'
+ %span.form-text.text-muted#support_help_block= _('URL of the Grafana instance to link to from the Metrics Dashboard menu item.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index b71e8ca831e..ecf3203df9a 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -7,20 +7,22 @@
.form-group
= f.label :help_page_text, _('Additional text to show on the Help page'), class: 'label-bold'
= f.text_area :help_page_text, class: 'form-control gl-form-input', rows: 4
- .form-text.text-muted= _('Markdown enabled')
+ .form-text.text-muted= _('Markdown enabled.')
.form-group
.form-check
= f.check_box :help_page_hide_commercial_content, class: 'form-check-input'
= f.label :help_page_hide_commercial_content, class: 'form-check-label' do
- = _('Hide marketing-related entries from the Help page.')
+ = _('Hide marketing-related entries from the Help page')
.form-group
= f.label :help_page_support_url, _('Support page URL'), class: 'label-bold'
- = f.text_field :help_page_support_url, class: 'form-control gl-form-input', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.form-text.text-muted#support_help_block= _('Alternate support URL for Help page and Help dropdown')
+ = f.text_field :help_page_support_url, class: 'form-control gl-form-input', placeholder: 'https://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
+ %span.form-text.text-muted#support_help_block= _('Alternate support URL for Help page and Help dropdown.')
- if show_documentation_base_url_field?
.form-group
= f.label :help_page_documentation_base_url, _('Documentation pages URL'), class: 'label-bold'
= f.text_field :help_page_documentation_base_url, class: 'form-control gl-form-input', placeholder: 'https://docs.gitlab.com'
-
+ - docs_link_url = help_page_path('user/admin_area/settings/help_page', anchor: 'destination-requirements')
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url }
+ %span.form-text.text-muted#support_help_block= html_escape(_('Requests for pages at %{code_start}%{help_text_url}%{code_end} redirect to the URL. The destination must meet certain requirements. %{docs_link_start}Learn more.%{docs_link_end}')) % { code_start: '<code>'.html_safe, help_text_url: help_url, code_end: '</code>'.html_safe, docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index b9da2047453..b22eef83876 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -6,7 +6,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Allow rendering of diagrams in AsciiDoc and Markdown documents using %{link}.').html_safe % { link: link_to('Kroki', 'https://kroki.io', target: '_blank') }
+ = _('Users can render diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents using Kroki.')
+ = link_to _('Learn more.'), help_page_path('administration/integration/kroki.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form', id: 'kroki-settings' } do |f|
= form_errors(@application_setting) if expanded
@@ -20,11 +21,15 @@
= f.label :kroki_url, 'Kroki URL', class: 'label-bold'
= f.text_field :kroki_url, class: 'form-control gl-form-input', placeholder: 'http://your-kroki-instance:8000'
.form-text.text-muted
- = (_('When Kroki is enabled, GitLab sends diagrams to an instance of Kroki to display them as images. You can use the free public cloud instance %{kroki_public_url} or you can %{install_link} on your own infrastructure. Once you\'ve installed Kroki, make sure to update the server URL to point to your instance.') % { kroki_public_url: '<code>https://kroki.io</code>', install_link: link_to('install Kroki', 'https://docs.kroki.io/kroki/setup/install/', target: '_blank') }).html_safe
+ - install_link_url = 'https://docs.kroki.io/kroki/setup/install/'
+ - install_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: install_link_url }
+ = html_escape(_('Use the public cloud instance URL (%{kroki_public_url}) or %{install_link_start}install Kroki%{install_link_end} on your own infrastructure and use your own instance URL.')) % { kroki_public_url: '<code>https://kroki.io</code>'.html_safe, install_link_start: install_link_start, install_link_end: '</a>'.html_safe }
.form-group
= f.label :kroki_formats, 'Additional diagram formats', class: 'label-bold'
.form-text.text-muted
- = (_('Using additional formats requires starting the companion containers. Make sure that all %{kroki_images} are running.') % { kroki_images: link_to('required containers', 'https://docs.kroki.io/kroki/setup/install/#_images', target: '_blank') }).html_safe
+ - container_link_url = 'https://docs.kroki.io/kroki/setup/install/#images'
+ - container_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: container_link_url }
+ = html_escape(_('To use the additional formats, you must start the required %{container_link_start}companion containers%{container_link_end}.')) % { container_link_start: container_link_start, container_link_end: '</a>'.html_safe }
- kroki_available_formats.each do |format|
.form-check
= f.check_box format[:name], class: 'form-check-input'
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
index 8de65f267d2..7cdadaaf37b 100644
--- a/app/views/admin/application_settings/_package_registry.html.haml
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -6,7 +6,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _("Settings related to the use and experience of using GitLab's Package Registry.")
+ = _("Control how the GitLab Package Registry functions.")
= render_if_exists 'admin/application_settings/ee_package_registry'
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 5d6443825b7..d14c8cffcc7 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -3,42 +3,48 @@
%fieldset
.form-group
- = f.label :max_pages_size, _('Maximum size of pages (MB)'), class: 'label-bold'
- = f.number_field :max_pages_size, class: 'form-control gl-form-input'
- .form-text.text-muted
- = _("0 for unlimited")
- .form-group
.form-check
= f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
= f.label :pages_domain_verification_enabled, class: 'form-check-label' do
- = _("Require users to prove ownership of custom domains")
+ = s_("AdminSettings|Require users to prove ownership of custom domains")
.form-text.text-muted
- = _("Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled")
- = link_to sprite_icon('question-o'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership')
+ - pages_link_url = help_page_path('administration/pages/index', anchor: 'custom-domain-verification')
+ - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
+ = s_('AdminSettings|Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. %{link_start}Learn more.%{link_end}').html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
- if Gitlab.config.pages.access_control
.form-group
.form-check
= f.check_box :force_pages_access_control, class: 'form-check-input'
= f.label :force_pages_access_control, class: 'form-check-label' do
- = _("Disable public access to Pages sites")
+ = s_("AdminSettings|Disable public access to Pages sites")
.form-text.text-muted
- = _("Access to Pages websites are controlled based on the user's membership to a given project. By checking this box, users will be required to be logged in to have access to all Pages websites in your instance.")
- = link_to sprite_icon('question-o'), help_page_path('administration/pages/index.md', anchor: 'disabling-public-access-to-all-pages-websites')
+ - pages_link_url = help_page_path('administration/pages/index', anchor: 'disable-public-access-to-all-pages-sites')
+ - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
+ = s_("AdminSettings|Select to disable public access for Pages sites, which requires users to sign in for access to the Pages sites in your instance. %{link_start}Learn more.%{link_end}").html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :max_pages_size, _('Maximum size of pages (MB)'), class: 'label-bold'
+ = f.number_field :max_pages_size, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ - pages_link_url = help_page_path('administration/pages/index', anchor: 'set-global-maximum-pages-size-per-project')
+ - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
+ = s_('AdminSettings|Set the maximum size of GitLab Pages per project (0 for unlimited). %{link_start}Learn more.%{link_end}').html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
%h5
- = _("Configure Let's Encrypt")
+ = s_("AdminSettings|Configure Let's Encrypt")
%p
- lets_encrypt_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: "https://letsencrypt.org/" }
- = _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
- .form-group
- = f.label :lets_encrypt_notification_email, _("Email"), class: 'label-bold'
- = f.text_field :lets_encrypt_notification_email, class: 'form-control gl-form-input'
- .form-text.text-muted
- = _("A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates.")
- .form-group
- .form-check
- = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
- = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
- - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
- = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
+ = _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA) that issues digital certificates to enable HTTPS (SSL/TLS) for sites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :lets_encrypt_notification_email, s_("AdminSettings|Let's Encrypt email"), class: 'label-bold'
+ = f.text_field :lets_encrypt_notification_email, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ - pages_link_url = help_page_path('administration/pages/index', anchor: 'lets-encrypt-integration')
+ - pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
+ = s_("AdminSettings|A Let's Encrypt account will be configured for this GitLab instance using this email address. You will receive emails to warn of expiring certificates. %{link_start}Learn more.%{link_end}").html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
+ .form-group
+ .form-check
+ = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
+ = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
+ - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
+ = s_("AdminSettings|I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF).").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index f603dcab407..ba7d747fa04 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -6,9 +6,9 @@
.form-check
= f.check_box :performance_bar_enabled, class: 'form-check-input', data: { qa_selector: 'enable_performance_bar_checkbox'}
= f.label :performance_bar_enabled, class: 'form-check-label' do
- = _("Enable access to the Performance Bar")
+ = _("Allow non-administrators to access to the performance bar")
.form-group
- = f.label :performance_bar_allowed_group_path, _('Allowed group'), class: 'label-bold'
+ = f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm qa-save-changes-button'
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 632aeec6ce3..39de15dc38d 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -6,7 +6,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
+ = _('Render diagrams in your documents using PlantUML.')
+ = link_to _('Learn more.'), help_page_path('administration/integration/plantuml.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form', id: 'plantuml-settings' } do |f|
= form_errors(@application_setting) if expanded
@@ -20,8 +21,6 @@
= 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
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
+ = _('The hostname of your PlantUML server.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index f102b3d580b..59690fdee8b 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -2,26 +2,23 @@
= form_errors(@application_setting)
%fieldset
- %p
- - 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 health and performance metrics endpoint")
+ .form-text.text-muted
+ = _('Enable a Prometheus endpoint that exposes health and performance statistics. The Health Check menu item appears in the Monitoring section of the Admin Area. Restart required.')
+ = link_to _('Learn More.'), help_page_path('administration/monitoring/prometheus/gitlab_metrics.md'), target: '_blank'
- unless Gitlab::Metrics.metrics_folder_present?
.form-text.text-muted
%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.
+ Only track method calls that take longer to complete than the given duration.
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 545c27d2a7e..6a7ec05d206 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -6,7 +6,6 @@
= f.label :polling_interval_multiplier, _('Polling interval multiplier'), class: 'label-bold'
= f.text_field :polling_interval_multiplier, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("Change this value to influence how frequently the GitLab UI polls for updates. If you set the value to 2 all polling intervals are multiplied by 2, which means that polling happens half as frequently. The multiplier can also have a decimal value. The default value (1) is a reasonable choice for the majority of GitLab installations. Set to 0 to completely disable polling.")
- = link_to sprite_icon('question-o'), help_page_path('administration/polling')
+ = _('Multiplier to apply to polling intervals. Decimal values are supported. Defaults to 1.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml
index f8ec04003fa..d962d050ebc 100644
--- a/app/views/admin/application_settings/_repository_static_objects.html.haml
+++ b/app/views/admin/application_settings/_repository_static_objects.html.haml
@@ -7,12 +7,12 @@
= _('External storage URL')
= f.text_field :static_objects_external_storage_url, class: 'form-control gl-form-input'
%span.form-text.text-muted#static_objects_external_storage_url_help_block
- = _('URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...).')
+ = _('URL of the external storage to serve the repository static objects.')
.form-group
= f.label :static_objects_external_storage_auth_token, class: 'label-bold' do
= _('External storage authentication token')
= f.text_field :static_objects_external_storage_auth_token, class: 'form-control gl-form-input'
%span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
- = _('A secure token that identifies an external storage request.')
+ = _('Secure token that identifies an external storage request.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 12a9f949750..156e7d3fb76 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -6,22 +6,22 @@
.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')
+ = _('Allow password authentication for the web interface')
.form-text.text-muted
- = _('When disabled, an external authentication provider must be used.')
+ = _('When inactive, 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)')
+ = _('Allow password authentication for Git over HTTP(S)')
.form-text.text-muted
- When disabled, a Personal Access Token
+ When inactive, a Personal Access Token
- if Gitlab::Auth::Ldap::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
%fieldset.form-group
- %legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth sign-in sources')
+ %legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources')
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
- oauth_providers_checkboxes.each do |source|
= source
@@ -30,39 +30,44 @@
.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')
+ = _('Enforce two-factor authentication')
+ %p.form-text.text-muted
+ = _('Enforce two-factor authentication for all user sign-ins.')
+ = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ .form-group
+ = f.label :two_factor_authentication, _('Two-factor grace period'), class: 'label-bold'
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control gl-form-input', placeholder: '0'
+ .form-text.text-muted
+ = _('Maximum time that users are allowed to skip the setup of two-factor authentication (in hours). Set to 0 (zero) to enforce at next sign in.')
.form-group
= f.label :admin_mode, _('Admin Mode'), class: 'label-bold'
= sprite_icon('lock', css_class: 'gl-icon')
.form-check
= f.check_box :admin_mode, class: 'form-check-input'
= f.label :admin_mode, class: 'form-check-label' do
- = _('Require additional authentication for administrative tasks')
- .form-text.text-muted
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode')
+ = _('Enable admin mode')
+ %p.form-text.text-muted
+ = _('Require additional authentication for administrative tasks.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
.form-check
= f.check_box :notify_on_unknown_sign_in, class: 'form-check-input'
= f.label :notify_on_unknown_sign_in, class: 'form-check-label' do
- = _('Notify users by email when sign-in location is not recognized')
- = link_to sprite_icon('question-o'),
- '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.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')
+ = _('Enable email notification')
+ %p.form-text.text-muted
+ = _('Notify users by email when sign-in location is not recognized.')
+ = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= 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= _("Direct non-authenticated users to this page.")
.form-group
- = f.label :after_sign_out_path, _('After sign-out path'), class: 'label-bold'
+ = f.label :after_sign_out_path, _('Sign-out page URL'), 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#home_help_block= _("Direct 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
+ %span.form-text.text-muted#home_help_block= _("Add text to the sign-in page. Markdown enabled.")
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index a5b47159239..a658ba63939 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -17,4 +17,5 @@
email_restrictions_enabled: @application_setting[:email_restrictions_enabled].to_s,
supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax',
email_restrictions: @application_setting.email_restrictions,
- after_sign_up_text: @application_setting[:after_sign_up_text] } }
+ after_sign_up_text: @application_setting[:after_sign_up_text],
+ pending_user_count: pending_user_count } }
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index fe260812ad9..fdf79004c45 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -6,13 +6,13 @@
.form-check
= f.check_box :enforce_terms, class: 'form-check-input'
= f.label :enforce_terms, class: 'form-check-label' do
- = _("Require all users to accept Terms of Service and Privacy Policy when they access GitLab.")
+ = _("All users must accept the Terms of Service and Privacy Policy to access GitLab")
.form-text.text-muted
- = _("When enabled, users cannot use GitLab until the terms have been accepted.")
.form-group
= f.label :terms do
= _("Terms of Service Agreement and Privacy Policy")
= f.text_area :terms, class: 'form-control gl-form-input', rows: 8
.form-text.text-muted
- = _("Markdown enabled")
+ = _("Markdown supported.")
+ = link_to _('What is Markdown?'), help_page_path('user/markdown.md'), target: '_blank', rel: 'noopener noreferrer'
= f.submit _("Save changes"), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 4bf47c3d60d..b6266c3ea34 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
+ = render_if_exists 'admin/application_settings/default_delayed_project_deletion_setting', form: f
= render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
diff --git a/app/views/admin/application_settings/appearances/_form.html.haml b/app/views/admin/application_settings/appearances/_form.html.haml
index a48b57bffd9..3bd16e4c344 100644
--- a/app/views/admin/application_settings/appearances/_form.html.haml
+++ b/app/views/admin/application_settings/appearances/_form.html.haml
@@ -40,7 +40,7 @@
= 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}.") % { favicon_extension_whitelist: favicon_extension_whitelist }
+ = _("Maximum file size is 1 MB. Image size must be 32 x 32 pixels. 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.")
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 53bdbcd7137..9102769cc6e 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -53,7 +53,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
+ = _('Set sign-in restrictions for all users.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'signin'
@@ -64,7 +65,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.')
+ = _('Add a Terms of Service agreement and Privacy Policy for users of this GitLab instance.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/terms.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terms'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 14483e4e55e..f1e37c76130 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -11,7 +11,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Enable and configure Prometheus metrics.')
+ = _('Monitor the health and performance of GitLab with Prometheus.')
.settings-content
= render 'prometheus'
@@ -22,7 +22,9 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Enable and configure Grafana.')
+ = _('Link to your Grafana instance.')
+ = link_to s_('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration.md'), target: '_blank', rel: 'noopener noreferrer'
+
.settings-content
= render 'grafana'
@@ -33,8 +35,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Enable access to the Performance Bar for a given group.')
- = link_to sprite_icon('question-o'), help_page_path('administration/monitoring/performance/performance_bar')
+ = _('Enable access to the performance bar for non-administrators in a given group.')
+ = link_to s_('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar.md', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'performance_bar'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 72a27e4523f..0e9dcb23dcb 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -68,7 +68,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Configure limit for issues created per minute by web and API requests.')
+ = _('Limit the number of issues and epics per minute a user can create through web and API requests.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/rate_limit_on_issues_creation.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'issue_limits'
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 0dfc3d7a60d..9711c335802 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -43,18 +43,19 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Size and domain settings for static websites')
+ = s_('AdminSettings|Size and domain settings for Pages static sites.')
.settings-content
= render 'pages'
%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
- = _('Real-time features')
+ = _('Polling interval multiplier')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Change this value to influence how frequently the GitLab UI polls for updates.')
+ = _('Adjust how frequently the GitLab UI polls for updates.')
+ = link_to _('Learn more.'), help_page_path('administration/polling.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'realtime'
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index 2a9fba1aef6..ac200002cd2 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -55,10 +55,11 @@
%section.settings.as-repository-static-objects.no-animate#js-repository-static-objects-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
- = _('Repository static objects')
+ = _('External storage for repository static objects')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN).')
+ = _('Serve repository static objects (for example, archives and blobs) from external storage.')
+ = link_to s_('Learn more.'), help_page_path('administration/static_objects_external_storage.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_static_objects'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index e7e17502da2..74eda21d5bd 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -40,5 +40,5 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
.form-actions
- = f.submit 'Submit', class: "gl-button btn btn-confirm wide"
- = link_to "Cancel", admin_applications_path, class: "gl-button btn btn-default btn-cancel"
+ = f.submit _('Save application'), class: "gl-button btn btn-confirm wide"
+ = link_to _('Cancel'), admin_applications_path, class: "gl-button btn btn-default btn-cancel"
diff --git a/app/views/admin/applications/new.html.haml b/app/views/admin/applications/new.html.haml
index 4d4b6b0c994..731cb51e2e4 100644
--- a/app/views/admin/applications/new.html.haml
+++ b/app/views/admin/applications/new.html.haml
@@ -1,6 +1,7 @@
-- breadcrumb_title _("Applications")
-- page_title _("New Application")
+- breadcrumb_title _("Add new application")
+- page_title _("Add new application")
-%h3.page-title New application
+%h3.page-title
+ = _("Add new application")
- @url = admin_applications_path
= render 'form', application: @application
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 8d643a7a4bc..8dcd5f81c23 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -3,47 +3,7 @@
%h3.page-title
Application: #{@application.name}
-.table-holder.oauth-application-show
- %table.table
- %tr
- %td
- = _('Application ID')
- %td
- .clipboard-group
- .input-group
- %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
- .input-group-append
- = clipboard_button(target: '#application_id', title: _("Copy ID"), class: "gl-button btn btn-default")
- %tr
- %td
- = _('Secret')
- %td
- .clipboard-group
- .input-group
- %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
- .input-group-append
- = clipboard_button(target: '#secret', title: _("Copy secret"), class: "gl-button btn btn-default")
- %tr
- %td
- = _('Callback URL')
- %td
- - @application.redirect_uri.split.each do |uri|
- %div
- %span.monospace= uri
- %tr
- %td
- Trusted
- %td
- = @application.trusted? ? 'Y' : 'N'
-
- %tr
- %td
- Confidential
- %td
- = @application.confidential? ? 'Y' : 'N'
-
- = render "shared/tokens/scopes_list", token: @application
-
-.form-actions
- = link_to 'Edit', edit_admin_application_path(@application), class: 'gl-button btn btn-confirm wide float-left'
- = render 'delete_form', application: @application, submit_btn_css: 'gl-button btn btn-danger gl-ml-3'
+= render 'shared/doorkeeper/applications/show',
+ edit_path: edit_admin_application_path(@application),
+ delete_path: admin_application_path(@application),
+ show_trusted_row: true
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index ec3daf6c494..97b3a757a3f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -118,6 +118,7 @@
- if Gitlab::CurrentSettings.version_check_enabled
.float-right
= version_status_badge
+ = link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
%p
= link_to _('GitLab'), general_admin_application_settings_path
%span.float-right
@@ -151,9 +152,9 @@
%span.float-right
#{Rails::VERSION::STRING}
%p
- = Gitlab::Database.human_adapter_name
+ = Gitlab::Database.main.human_adapter_name
%span.float-right
- = Gitlab::Database.version
+ = Gitlab::Database.main.version
%p
= _('Redis')
%span.float-right
diff --git a/app/views/admin/dev_ops_report/_callout.html.haml b/app/views/admin/dev_ops_report/_callout.html.haml
deleted file mode 100644
index 2b4c258a00c..00000000000
--- a/app/views/admin/dev_ops_report/_callout.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.gl-mt-3
-.user-callout{ data: { uid: 'dev_ops_report_intro_callout_dismissed' } }
- .bordered-box.landing.content-block
- %button.gl-button.btn.btn-default-tertiary.close.js-close-callout{ type: 'button',
- 'aria-label' => _('Dismiss DevOps Report introduction') }
- = sprite_icon('close', size: 16, css_class: 'dismiss-icon')
- .user-callout-copy
- %h4
- = _('Introducing Your DevOps Report')
- %p
- = _('Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.')
- .svg-container.devops
- = custom_icon('dev_ops_report_overview')
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index 0b26548d6e6..208afefc73b 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -1,9 +1,6 @@
- service_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
-- if service_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
- = render 'callout'
-
- if !service_ping_enabled
#js-devops-service-ping-disabled{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_service_ping_path: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/service_ping/index.md') } }
- else
- #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') } }
+ #js-devops-score{ data: { devops_score_metrics: devops_score_metrics(@metric).to_json, no_data_image_path: image_path('dev_ops_report_no_data.svg'), devops_score_intro_image_path: image_path('dev_ops_report_overview.svg') } }
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index e7e0e58f6fb..91a018121c0 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [:admin, @group] do |f|
+= gitlab_ui_form_for [:admin, @group] do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
= render 'shared/group_form_description', f: f
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 9b42e1b4967..ae809f01592 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -4,7 +4,6 @@
- page_title @group.name, _("Groups")
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
-.js-remove-member-modal
%h3.page-title
= _('Group: %{group_name}') % { group_name: @group.full_name }
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 5c92cbf957e..1a87b21351c 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -5,7 +5,6 @@
- @content_class = "admin-projects"
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
-.js-remove-member-modal
%h3.page-title
= _('Project: %{name}') % { name: @project.full_name }
= link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
deleted file mode 100644
index ce143a6b155..00000000000
--- a/app/views/admin/runners/_runner.html.haml
+++ /dev/null
@@ -1,80 +0,0 @@
--# Note: This file should stay aligned with:
--# `app/views/groups/runners/_runner.html.haml`
-
-.gl-responsive-table-row{ data: { testid: "runner-row-#{runner.id}" } }
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Type')
- .table-mobile-content
- - if runner.instance_type?
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|shared')
- - elsif runner.group_type?
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|group')
- - else
- %span.badge.badge-pill.gl-badge.sm.badge-info= s_('Runners|specific')
- - if runner.locked?
- %span.badge.badge-pill.gl-badge.sm.badge-warning= s_('Runners|locked')
- - unless runner.active?
- %span.badge.badge-pill.gl-badge.sm.badge-danger= s_('Runners|paused')
-
- .table-section.section-30
- .table-mobile-header{ role: 'rowheader' }= s_('Runners|Runner')
- .table-mobile-content
- = link_to("##{runner.id} (#{runner.short_sha})", admin_runner_path(runner))
- .gl-text-truncate
- %span{ title: runner.description, data: { toggle: 'tooltip', container: 'body' } }
- = runner.description
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Version')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.version }
- = runner.version
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('IP Address')
- .table-mobile-content.str-truncated.has-tooltip{ title: runner.ip_address }
- = runner.ip_address
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Projects')
- .table-mobile-content
- - if runner.instance_type? || runner.group_type?
- = _('n/a')
- - else
- = runner.projects.count(:all)
-
- .table-section.section-5
- .table-mobile-header{ role: 'rowheader' }= _('Jobs')
- .table-mobile-content
- = limited_counter_with_delimiter(runner.builds)
-
- .table-section.section-10.section-wrap
- .table-mobile-header{ role: 'rowheader' }= _('Tags')
- .table-mobile-content
- - runner.tags.map(&:name).sort.each do |tag|
- %span.badge.badge-primary.str-truncated.has-tooltip{ title: tag }
- = tag
-
- .table-section.section-10
- .table-mobile-header{ role: 'rowheader' }= _('Last contact')
- .table-mobile-content
- - contacted_at = runner_contacted_at(runner)
- - if contacted_at
- = time_ago_with_tooltip contacted_at
- - else
- = _('Never')
-
- .table-section.table-button-footer.section-10
- .btn-group.table-action-buttons
- .btn-group
- = link_to admin_runner_path(runner), class: 'gl-button btn btn-default btn-icon has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
- = sprite_icon('pencil', css_class: 'gl-icon')
- .btn-group
- - if runner.active?
- = link_to [:pause, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-icon has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = sprite_icon('pause', css_class: 'gl-icon')
- - else
- = link_to [:resume, :admin, runner], method: :post, class: 'gl-button btn btn-default btn-icon has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
- = sprite_icon('play', css_class: 'gl-icon')
- .btn-group
- = link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger btn-icon has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = sprite_icon('close', css_class: 'gl-icon')
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index f9c52d9316b..f298fce7bcf 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,140 +1,4 @@
- breadcrumb_title _('Runners')
- page_title _('Runners')
-- if Feature.enabled?(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
- #js-runner-list{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
-- else
- .row
- .col-sm-6
- .bs-callout
- %p
- = _("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
-
- %div
- %span= _('Runners can be:')
- %ul
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|shared')
- \-
- = _('Runs jobs from all unassigned projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-success= s_('Runners|group')
- \-
- = _('Runs jobs from all unassigned projects in its group.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-info= s_('Runners|specific')
- \-
- = _('Runs jobs from assigned projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-warning= s_('Runners|locked')
- \-
- = _('Cannot be assigned to other projects.')
- %li
- %span.badge.badge-pill.gl-badge.sm.badge-danger= s_('Runners|paused')
- \-
- = _('Not available to run jobs.')
-
- .col-sm-6
- .bs-callout
- = render partial: 'ci/runner/how_to_setup_runner',
- locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
- type: s_('Runners|shared'),
- reset_token_url: reset_registration_token_admin_application_settings_path,
- project_path: '',
- group_path: '' }
-
- .row
- .col-sm-9
- = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
- .filtered-search-wrapper.d-flex
- .filtered-search-box
- = dropdown_tag(_('Recent searches'),
- options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
- toggle_class: 'gl-button btn btn-default filtered-search-history-dropdown-toggle-button',
- dropdown_class: 'filtered-search-history-dropdown',
- content_class: 'filtered-search-history-dropdown-content' }) do
- .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
- .filtered-search-box-input-container.droplab-dropdown
- .scroll-container
- %ul.tokens-container.list-unstyled
- %li.input-token
- %input.form-control.filtered-search{ search_filter_input_options('runners') }
- #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
- = button_tag class: %w[gl-button btn btn-link] do
- -# Encapsulate static class name `{{icon}}` inside #{} to bypass
- -# haml lint's ClassAttributeWithStaticValue
- %svg
- %use{ 'xlink:href': "#{'{{icon}}'}" }
- %span.js-filter-hint
- {{formattedKey}}
- #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
- %li.filter-dropdown-item{ data: { value: "{{ title }}" } }
- %button.gl-button.btn.btn-link{ type: 'button' }
- {{ title }}
- %span.btn-helptext
- {{ help }}
- #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_STATUSES.each do |status|
- %li.filter-dropdown-item{ data: { value: status } }
- = button_tag class: %w[gl-button btn btn-link] do
- = status.titleize
-
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: %w[gl-button btn btn-link] do
- = runner_type.titleize
-
- #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
- %li.filter-dropdown-item{ data: { value: runner_type } }
- = button_tag class: %w[gl-button btn btn-link] do
- = runner_type.titleize
-
- #js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'none' } }
- %button.gl-button.btn.btn-link
- = _('No Tag')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.gl-button.btn.btn-link.js-data-value
- %span.dropdown-light-content
- {{name}}
-
- = button_tag class: %w[clear-search hidden] do
- = sprite_icon('close', size: 16, css_class: 'clear-search-icon')
- .filter-dropdown-container
- = render 'sort_dropdown'
-
- .col-sm-3.text-right-lg
- = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- - if @runners.any?
- .content-list{ data: { testid: 'runners-table' } }
- .table-holder
- .gl-responsive-table-row.table-row-header{ role: 'row' }
- .table-section.section-10{ role: 'rowheader' }= _('Type/State')
- .table-section.section-30{ role: 'rowheader' }= s_('Runners|Runner')
- .table-section.section-10{ role: 'rowheader' }= _('Version')
- .table-section.section-10{ role: 'rowheader' }= _('IP Address')
- .table-section.section-5{ role: 'rowheader' }= _('Projects')
- .table-section.section-5{ role: 'rowheader' }= _('Jobs')
- .table-section.section-10{ role: 'rowheader' }= _('Tags')
- .table-section.section-10{ role: 'rowheader' }= _('Last contact')
- .table-section.section-10{ role: 'rowheader' }
-
- - @runners.each do |runner|
- = render 'admin/runners/runner', runner: runner
- = paginate @runners, theme: 'gitlab'
- - else
- .nothing-here-block= _('No runners found')
+#js-admin-runners{ data: { registration_token: Gitlab::CurrentSettings.runners_registration_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', active_runners_count: @active_runners_count } }
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
deleted file mode 100644
index 4d9fa6d3d57..00000000000
--- a/app/views/admin/services/_form.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-= render "service_templates_deprecated_alert"
-
-%h3.page-title
- = @service.title
-
-%p= @service.description
-
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
- = render 'shared/service_settings', form: form, integration: @service
diff --git a/app/views/admin/services/_service_templates_deprecated_alert.html.haml b/app/views/admin/services/_service_templates_deprecated_alert.html.haml
deleted file mode 100644
index eac2f9c7f4e..00000000000
--- a/app/views/admin/services/_service_templates_deprecated_alert.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
-- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
-
-.gl-alert.gl-alert-danger.gl-mt-5{ role: 'alert' }
- .gl-alert-container
- = sprite_icon('error', css_class: 'gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content
- %h4.gl-alert-title= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
- .gl-alert-body
- = html_escape_once(s_("AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}.")).html_safe % { settings_link_start: settings_link_start, doc_link_start: doc_link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml
deleted file mode 100644
index d13b5a34dac..00000000000
--- a/app/views/admin/services/edit.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- add_to_breadcrumbs _("Service Templates"), admin_application_settings_services_path
-- page_title @service.title, _("Service Templates")
-- breadcrumb_title @service.title
-- @content_class = 'limit-container-width' unless fluid_layout
-
-= render 'form'
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
deleted file mode 100644
index 91706452402..00000000000
--- a/app/views/admin/services/index.html.haml
+++ /dev/null
@@ -1,43 +0,0 @@
-- page_title _("Service Templates")
-- @content_class = 'limit-container-width' unless fluid_layout
-
-= render "service_templates_deprecated_alert"
-
-- if @activated_services.any?
- %h3.page-title Service templates
- %p= s_('AdminSettings|Service template allows you to set default values for integrations')
-
- %table.table.b-table.gl-table
- %colgroup
- %col
- %col
- %col
- %col{ width: 135 }
- %thead
- %tr
- %th
- %th= _('Service')
- %th= _('Description')
- %th= _('Last edit')
- - @activated_services.each do |service|
- - if service.type.in?(@existing_instance_types)
- %tr
- %td
- %td
- = link_to edit_admin_application_settings_integration_path(service.to_param), class: 'gl-text-blue-300!' do
- %strong.has-tooltip{ title: s_('AdminSettings|Moved to integrations'), data: { container: 'body' } }
- = service.title
- %td.gl-cursor-default.gl-text-gray-400
- = service.description
- %td
- - else
- %tr
- %td
- = boolean_to_icon service.activated?
- %td
- = link_to edit_admin_application_settings_service_path(service.id) do
- %strong= service.title
- %td
- = service.description
- %td.light
- = time_ago_with_tooltip service.updated_at
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index cddea17efbf..24048a8b328 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -1,7 +1,6 @@
- link = link_to _("Install GitLab Runner and ensure it's running."), 'https://docs.gitlab.com/runner/install/', target: '_blank'
.gl-mb-3
- %h5= _("Set up a %{type} runner manually") % { type: type }
-
+ %h5= _("Set up a %{type} Runner for a project") % { type: type }
%ol
%li
= link.html_safe
diff --git a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml b/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml
deleted file mode 100644
index 7140c0f4e7c..00000000000
--- a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-%h5= _('Set up a %{type} runner automatically') % { type: type }
-
-%p
- - link_to_help_page = link_to(_('Learn more.'),
- help_page_path('user/project/clusters/index'),
- target: '_blank',
- rel: 'noopener noreferrer')
-
- = _('Register a runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
-
-%ol
- %li
- = _('Click the button below.')
- %li
- = _('Select an existing Kubernetes cluster or create a new one.')
- %li
- = _('From the Kubernetes cluster details view, applications list, install GitLab Runner.')
-
-= link_to _('Install GitLab Runner on Kubernetes'),
- clusters_path,
- class: 'gl-button btn btn-info'
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 9db5ee23c3e..cdfc174ebf1 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -16,6 +16,7 @@
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-the-aws-elastic-container-service-ecs'),
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'run-aws-commands-from-gitlab-cicd'),
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'aws'),
+ contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'use-variables-or-in-other-variables'),
protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protect-a-cicd-variable'),
masked_environment_variables_link: help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'),
} }
diff --git a/app/views/clusters/clusters/_integrations.html.haml b/app/views/clusters/clusters/_integrations.html.haml
index 96219fa9de5..f136091dad5 100644
--- a/app/views/clusters/clusters/_integrations.html.haml
+++ b/app/views/clusters/clusters/_integrations.html.haml
@@ -5,25 +5,23 @@
.settings-content#integrations-settings-section
- if can?(current_user, :admin_cluster, @cluster)
.sub-section.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|
+ = gitlab_ui_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
- = 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
- = 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'
+ - help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Prometheus for metrics.')
+ - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "prometheus-cluster-integration"), target: '_blank', rel: 'noopener noreferrer')
+ = prometheus_form.gitlab_ui_checkbox_component :enabled,
+ s_('ClusterIntegration|Enable Prometheus integration'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
= 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|
+ = gitlab_ui_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'
+ - help_text = s_('ClusterIntegration|Allows GitLab to query a specifically configured in-cluster Elasticsearch for pod logs.')
+ - help_link = link_to(_('More information.'), help_page_path("user/clusters/integrations", anchor: "elastic-stack-cluster-integration"), target: '_blank', rel: 'noopener noreferrer')
+ = elastic_stack_form.gitlab_ui_checkbox_component :enabled,
+ s_('ClusterIntegration|Enable Elastic Stack integration'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
= elastic_stack_form.submit _('Save changes'), class: 'btn gl-button btn-success'
diff --git a/app/views/clusters/clusters/_multiple_clusters_message.html.haml b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
index f235435d907..ed95744c11d 100644
--- a/app/views/clusters/clusters/_multiple_clusters_message.html.haml
+++ b/app/views/clusters/clusters/_multiple_clusters_message.html.haml
@@ -1,4 +1,4 @@
-- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'use-multiple-kubernetes-clusters')
+- autodevops_help_url = help_page_path('topics/autodevops/multiple_clusters_auto_devops.md')
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
- help_link_end = '</a>'.html_safe
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 90a49e4bbe3..fdaf2107686 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -9,7 +9,7 @@
- if current_user.can_create_project?
.page-title-controls
- = link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm"
+ = link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm", data: { qa_selector: 'new_project_button' }
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= sprite_icon('chevron-lg-left', size: 12)
diff --git a/app/views/devise/shared/_email_opted_in.html.haml b/app/views/devise/shared/_email_opted_in.html.haml
index 6896ef21536..3817f9f651d 100644
--- a/app/views/devise/shared/_email_opted_in.html.haml
+++ b/app/views/devise/shared/_email_opted_in.html.haml
@@ -1,6 +1,6 @@
-- is_hidden = local_assigns.fetch(:hidden, Gitlab.dev_env_or_com?)
+- return unless Gitlab.dev_env_or_com?
-.gl-mb-3.js-email-opt-in{ class: is_hidden ? 'hidden' : '' }
+.gl-mb-3.js-email-opt-in.hidden
.gl-font-weight-bold.gl-mb-3
= _('Email updates (optional)')
= f.check_box :email_opted_in
diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml
index 3bc2146b313..8d6e043ebf7 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -30,7 +30,8 @@
id: 'import_group_path',
required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- title: _('Please choose a group URL with no special characters.'),
+ title: group_url_error_message,
+ maxlength: ::Namespace::URL_MAX_LENGTH,
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
%p.validation-error.gl-field-error.field-validation.hide
= _("Group path is already taken. We've suggested one that is available.")
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index 3be1a142ca6..78f079df158 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -1,8 +1,6 @@
-- return unless can_manage_members?(group)
+- return unless can_admin_group_member?(group)
-.js-invite-members-modal{ data: { id: group.id,
- name: group.name,
- is_project: 'false',
+.js-invite-members-modal{ data: { is_project: 'false',
access_levels: GroupMember.access_level_roles.to_json,
default_access_level: Gitlab::Access::GUEST,
- help_link: help_page_url('user/permissions') }.merge(group_select_data(group)) }
+ help_link: help_page_url('user/permissions') }.merge(group_select_data(group)).merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
diff --git a/app/views/groups/dependency_proxies/_url.html.haml b/app/views/groups/dependency_proxies/_url.html.haml
index 785ad8f94fd..a8034c50ed8 100644
--- a/app/views/groups/dependency_proxies/_url.html.haml
+++ b/app/views/groups/dependency_proxies/_url.html.haml
@@ -1,6 +1,6 @@
-- proxy_url = group_dependency_proxy_url(@group)
+- proxy_url = group_dependency_proxy_image_prefix(@group)
-%h5.prepend-top-20= _('Dependency proxy URL')
+%h5.prepend-top-20= _('Dependency proxy image prefix')
.row
.col-lg-8.col-md-12.input-group
@@ -8,5 +8,5 @@
= clipboard_button(text: "#{proxy_url}", title: _("Copy %{proxy_url}") % { proxy_url: proxy_url })
.row
- .col-12.help-block.gl-mt-3
+ .col-12.help-block.gl-mt-3{ data: { qa_selector: 'dependency_proxy_count' } }
= _('Contains %{count} blobs of images (%{size})') % { count: @blobs_count, size: number_to_human_size(@blobs_total_size) }
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index 5cf4ff4bc26..177018af830 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -13,7 +13,7 @@
.form-group
%h5.prepend-top-20= _('Enable proxy')
.js-dependency-proxy-toggle-area
- = render "shared/buttons/project_feature_toggle", is_checked: @dependency_proxy.enabled?, label: s_("DependencyProxy|Toggle Dependency Proxy") do
+ = render "shared/buttons/project_feature_toggle", is_checked: @dependency_proxy.enabled?, label: s_("DependencyProxy|Toggle Dependency Proxy"), data: { qa_selector: 'dependency_proxy_setting_toggle' } do
= f.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
- if @dependency_proxy.enabled
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index c5b8c5e25a3..97867e312af 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -2,11 +2,10 @@
- page_title _('Group members')
- groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups })
-.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap
- - if can_manage_members?
+ - if can_admin_group_member?(@group)
.gl-w-half.gl-xs-w-full
%h4
= _('Group members')
@@ -21,7 +20,7 @@
trigger_source: 'group-members-page',
display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- - if can_manage_members? && Feature.disabled?(:invite_members_group_modal, @group)
+ - if can_admin_group_member?(@group) && 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' }
@@ -36,9 +35,9 @@
= render_if_exists 'groups/group_members/ldap_sync'
- .js-group-members-list-app{ data: { members_data: group_members_app_data_json(@group,
- members: @members,
- invited: @invited_members,
- access_requests: @requesters) } }
+ .js-group-members-list-app{ data: { members_data: group_members_app_data(@group,
+ members: @members,
+ invited: @invited_members,
+ access_requests: @requesters).to_json } }
.loading
.gl-spinner.gl-spinner-md
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
index 49e297ee13d..1cccce9f59a 100644
--- a/app/views/groups/runners/_group_runners.html.haml
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -3,17 +3,14 @@
%h4
= _('Group runners')
-%p
- = _('These runners are shared across projects in this group.')
- = _('Group runners can be managed with the %{link}.').html_safe % { link: link }
-
-# Proper policies should be implemented per
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/45894
.bs-callout.help-callout
+ %p
+ = _('These runners are shared across projects in this group.')
+ = _('Group runners can be managed with the %{link}.').html_safe % { link: link }
+
- if can?(current_user, :admin_pipeline, @group) && valid_runner_registrars.include?('group')
- = render partial: 'ci/runner/how_to_setup_runner_automatically',
- locals: { type: 'group',
- clusters_path: group_clusters_path(@group) }
- if params[:ci_runner_templates]
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
diff --git a/app/views/groups/runners/_settings.html.haml b/app/views/groups/runners/_settings.html.haml
index 187588f5f11..55960703f9a 100644
--- a/app/views/groups/runners/_settings.html.haml
+++ b/app/views/groups/runners/_settings.html.haml
@@ -75,7 +75,7 @@
= button_tag class: 'clear-search hidden' do
= sprite_icon('close', size: 16, css_class: 'clear-search-icon')
.filter-dropdown-container
- = render 'admin/runners/sort_dropdown'
+ = render 'groups/runners/sort_dropdown'
.col-sm-3.text-right-lg
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: limited_counter_with_delimiter(@all_group_runners.online) }
@@ -96,6 +96,7 @@
.table-section.section-10{ role: 'rowheader' }
- @group_runners.each do |runner|
+ - runner = runner.present(current_user: current_user)
= render 'groups/runners/runner', runner: runner
= paginate @group_runners, theme: 'gitlab', :params => { :anchor => 'runners-settings' }
- else
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/groups/runners/_sort_dropdown.html.haml
index c6627ae0f27..e914bd00dac 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/groups/runners/_sort_dropdown.html.haml
@@ -1,4 +1,4 @@
-- sorted_by = sort_options_hash[@sort]
+- sorted_by = sort_options_hash[@sort] || sort_title_created_date
.dropdown.inline.gl-ml-3
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
@@ -8,4 +8,3 @@
%li
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sorted_by)
= sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date), sorted_by)
-
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
new file mode 100644
index 00000000000..4e7bc99b1f0
--- /dev/null
+++ b/app/views/groups/runners/index.html.haml
@@ -0,0 +1,6 @@
+- page_title s_('Runners|Runners')
+
+%h2.page-title
+ = s_('Runners|Group Runners')
+
+#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } }
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index fea0736ffc8..cdff533e3c7 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -20,10 +20,12 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- title: s_('GroupSettings|Please choose a group URL with no special characters.'),
+ title: group_url_error_message,
+ maxlength: ::Namespace::URL_MAX_LENGTH,
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
= f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning'
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group
= render_if_exists 'groups/settings/restore', group: @group
+= render_if_exists 'groups/settings/immediately_remove', group: @group
diff --git a/app/views/groups/settings/_lfs.html.haml b/app/views/groups/settings/_lfs.html.haml
index b16c9faafa4..1255a2901ea 100644
--- a/app/views/groups/settings/_lfs.html.haml
+++ b/app/views/groups/settings/_lfs.html.haml
@@ -6,8 +6,7 @@
%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
- .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.')
+ = f.gitlab_ui_checkbox_component :lfs_enabled,
+ _('Allow projects within this group to use Git LFS'),
+ help_text: _('This setting can be overridden in each project.'),
+ checkbox_options: { checked: @group.lfs_enabled?, data: { qa_selector: 'lfs_checkbox' } }
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index d1f356ed665..683e70248b6 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -1,4 +1,4 @@
-= form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f|
+= gitlab_ui_form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-permissions-settings' }
= form_errors(@group)
@@ -9,34 +9,28 @@
- if @group.root?
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :prevent_sharing_groups_outside_hierarchy, disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group), class: 'custom-control-input'
- = f.label :prevent_sharing_groups_outside_hierarchy, class: 'custom-control-label' do
- %span
- = s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) }
- %p.js-descr.help-text= prevent_sharing_groups_outside_hierarchy_help_text(@group)
+ = f.gitlab_ui_checkbox_component :prevent_sharing_groups_outside_hierarchy,
+ s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) },
+ help_text: prevent_sharing_groups_outside_hierarchy_help_text(@group),
+ checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) }
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input'
- = f.label :share_with_group_lock, class: 'custom-control-label' do
- %span
- = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: link_to_group(@group) }
- %p.js-descr.help-text= share_with_group_lock_help_text(@group)
+ = f.gitlab_ui_checkbox_component :share_with_group_lock,
+ s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: link_to_group(@group) },
+ checkbox_options: { disabled: !can_change_share_with_group_lock?(@group) },
+ help_text: share_with_group_lock_help_text(@group)
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input'
- = f.label :emails_disabled, class: 'custom-control-label' do
- %span= s_('GroupSettings|Disable email notifications')
- %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
+ = f.gitlab_ui_checkbox_component :emails_disabled,
+ s_('GroupSettings|Disable email notifications'),
+ checkbox_options: { checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group) },
+ help_text: s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input'
- = f.label :mentions_disabled, class: 'custom-control-label' do
- %span= s_('GroupSettings|Disable group mentions')
- %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
+ = f.gitlab_ui_checkbox_component :mentions_disabled,
+ s_('GroupSettings|Disable group mentions'),
+ checkbox_options: { checked: @group.mentions_disabled? },
+ help_text: s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
diff --git a/app/views/groups/settings/_project_access_token_creation.html.haml b/app/views/groups/settings/_project_access_token_creation.html.haml
index 8be17c6cc30..948b25390ba 100644
--- a/app/views/groups/settings/_project_access_token_creation.html.haml
+++ b/app/views/groups/settings/_project_access_token_creation.html.haml
@@ -1,10 +1,9 @@
- return unless render_setting_to_allow_project_access_token_creation?(group)
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
- = f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do
- %span= s_('GroupSettings|Allow project access token creation')
- - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
- - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
- %p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ - project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
+ = f.gitlab_ui_checkbox_component :resource_access_token_creation_allowed,
+ s_('GroupSettings|Allow project access token creation'),
+ checkbox_options: { checked: group.namespace_settings.resource_access_token_creation_allowed?, data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' } },
+ help_text: s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml
index bd3b3283288..9e5eeee2e2a 100644
--- a/app/views/groups/settings/_two_factor_auth.html.haml
+++ b/app/views/groups/settings/_two_factor_auth.html.haml
@@ -7,17 +7,15 @@
%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-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')
+ = f.gitlab_ui_checkbox_component :require_two_factor_authentication,
+ _('Require all users in this group to setup two-factor authentication'),
+ checkbox_options: { data: { qa_selector: 'require_2fa_checkbox' } }
.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
- .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')
+ = f.gitlab_ui_checkbox_component :allow_mfa_for_subgroups,
+ _('Allow subgroups to set up their own two-factor authentication rules'),
+ checkbox_options: { checked: group.namespace_settings&.allow_mfa_for_subgroups }
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
deleted file mode 100644
index e0158e4bf22..00000000000
--- a/app/views/groups/sidebar/_packages.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-- packages_link = group_packages_list_nav? ? group_packages_path(@group) : group_container_registries_path(@group)
-
-- if group_packages_nav?
- = nav_link(controller: ['groups/packages', 'groups/registry/repositories', 'groups/dependency_proxies']) do
- = link_to packages_link, title: _('Packages'), class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('package')
- %span.nav-item-name
- = _('Packages & Registries')
- %ul.sidebar-sub-level-items
- = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
- = link_to packages_link, title: _('Packages & Registries') do
- %strong.fly-out-top-item-name
- = _('Packages & Registries')
- %li.divider.fly-out-top-item
- - if group_packages_list_nav?
- = nav_link(controller: 'groups/packages') do
- = link_to group_packages_path(@group), title: _('Packages') do
- %span= _('Package Registry')
- - if group_container_registry_nav?
- = nav_link(controller: 'groups/registry/repositories') do
- = link_to group_container_registries_path(@group), title: _('Container Registry') do
- %span= _('Container Registry')
- - if group_dependency_proxy_nav?
- = nav_link(controller: 'groups/dependency_proxies') do
- = link_to group_dependency_proxy_path(@group), title: _('Dependency Proxy') do
- %span= _('Dependency Proxy')
diff --git a/app/views/groups/sidebar/_packages_settings.html.haml b/app/views/groups/sidebar/_packages_settings.html.haml
deleted file mode 100644
index 78533aba75f..00000000000
--- a/app/views/groups/sidebar/_packages_settings.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-- if group_packages_list_nav?
- = nav_link(controller: :packages_and_registries) do
- = link_to group_settings_packages_and_registries_path(@group), title: _('Packages & Registries'), data: { qa_selector: 'group_package_settings_link' } do
- %span
- = _('Packages & Registries')
diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml
index 1cd05dcf65e..88c531535b4 100644
--- a/app/views/help/instance_configuration.html.haml
+++ b/app/views/help/instance_configuration.html.haml
@@ -8,6 +8,8 @@
= render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci'
+ = render 'help/instance_configuration/package_registry'
+ = render 'help/instance_configuration/rate_limits'
%p
%strong= _("Table of contents")
diff --git a/app/views/help/instance_configuration/_package_registry.html.haml b/app/views/help/instance_configuration/_package_registry.html.haml
new file mode 100644
index 00000000000..38202b8d6e6
--- /dev/null
+++ b/app/views/help/instance_configuration/_package_registry.html.haml
@@ -0,0 +1,48 @@
+- package_file_size_limits = @instance_configuration.settings[:package_file_size_limits]
+- content_for :table_content do
+ - if package_file_size_limits.present?
+ %li= link_to _('Package Registry'), '#package-registry'
+
+- content_for :settings_content do
+ - if package_file_size_limits.present?
+ %h2#package-registry
+ = _('Package Registry')
+
+ %p
+ = _('There are several file size limits in place for the Package Registry.')
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th= _('Package type')
+ - package_file_size_limits.each_key do |title|
+ %th= title
+ %tbody
+ %tr
+ %td= 'Conan'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:conan])
+ %tr
+ %td= 'Maven'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:maven])
+ %tr
+ %td= 'npm'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:npm])
+ %tr
+ %td= 'NuGet'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:nuget])
+ %tr
+ %td= 'PyPI'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:pypi])
+ %tr
+ %td= 'Terraform Module'
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:terraform_module])
+ %tr
+ %td= _('Generic')
+ - package_file_size_limits.each_value do |limits|
+ %td= instance_configuration_human_size_cell(limits[:generic])
diff --git a/app/views/help/instance_configuration/_rate_limit_row.html.haml b/app/views/help/instance_configuration/_rate_limit_row.html.haml
new file mode 100644
index 00000000000..85c165de7d4
--- /dev/null
+++ b/app/views/help/instance_configuration/_rate_limit_row.html.haml
@@ -0,0 +1,7 @@
+- public_visible = local_assigns.fetch(:public_visible, false)
+
+- if rate_limit && (public_visible || user_signed_in?)
+ %tr
+ %td= title
+ %td= instance_configuration_cell_html(rate_limit[:enabled] ? rate_limit[:requests_per_period] : nil)
+ %td= instance_configuration_cell_html(rate_limit[:enabled] ? rate_limit[:period_in_seconds] : nil)
diff --git a/app/views/help/instance_configuration/_rate_limits.html.haml b/app/views/help/instance_configuration/_rate_limits.html.haml
new file mode 100644
index 00000000000..d72bd845c5b
--- /dev/null
+++ b/app/views/help/instance_configuration/_rate_limits.html.haml
@@ -0,0 +1,36 @@
+- rate_limits = @instance_configuration.settings[:rate_limits]
+- content_for :table_content do
+ - if rate_limits
+ %li= link_to _('Rate Limits'), '#rate-limits'
+
+- content_for :settings_content do
+ - if rate_limits
+ %h2#rate-limits
+ = _('Rate Limits')
+
+ %p
+ = _('There are several rate limits in place to protect the system.')
+ .table-responsive
+ %table
+ %thead
+ %tr
+ %th= _('Rate limit')
+ %th= _('Requests per period')
+ %th= _('Period in seconds')
+ %tbody
+ = render 'help/instance_configuration/rate_limit_row', title: _('Unauthenticated requests'), rate_limit: rate_limits[:unauthenticated], public_visible: true
+ = render 'help/instance_configuration/rate_limit_row', title: _('Authenticated API requests'), rate_limit: rate_limits[:authenticated_api]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Authenticated web requests'), rate_limit: rate_limits[:authenticated_web]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Protected Paths: requests'), rate_limit: rate_limits[:protected_paths]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Package Registry: unauthenticated API requests'), rate_limit: rate_limits[:unauthenticated_packages_api], public_visible: true
+ = render 'help/instance_configuration/rate_limit_row', title: _('Package Registry: authenticated API requests'), rate_limit: rate_limits[:authenticated_packages_api]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Issue creation requests'), rate_limit: rate_limits[:issue_creation]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Note creation requests'), rate_limit: rate_limits[:note_creation]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Project export requests'), rate_limit: rate_limits[:project_export]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Project export download requests'), rate_limit: rate_limits[:project_export_download]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Project import requests'), rate_limit: rate_limits[:project_import]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Group export requests'), rate_limit: rate_limits[:group_export]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Group export download requests'), rate_limit: rate_limits[:group_export_download]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Group import requests'), rate_limit: rate_limits[:group_import]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Raw blob requests'), rate_limit: rate_limits[:raw_blob]
+ %br
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index cd90c76ed10..2aae8811678 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -1,10 +1,11 @@
- add_to_breadcrumbs _('New group'), new_group_path
- add_page_specific_style 'page_bundles/import'
-- breadcrumb_title _('Import groups')
+- page_title _('Import groups')
#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),
jobs_path: realtime_changes_import_bulk_imports_path(format: :json),
source_url: @source_url,
- group_path_regex: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS } }
+ group_path_regex: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
+ group_url_error_message: group_url_error_message } }
diff --git a/app/views/jira_connect/branches/new.html.haml b/app/views/jira_connect/branches/new.html.haml
new file mode 100644
index 00000000000..f0e34c30018
--- /dev/null
+++ b/app/views/jira_connect/branches/new.html.haml
@@ -0,0 +1,5 @@
+- @hide_breadcrumbs = true
+- @hide_top_links = true
+- page_title _('New branch')
+
+.js-jira-connect-create-branch{ data: @new_branch_data }
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index 43672551caf..cbe9a860210 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -24,9 +24,10 @@
%p.jira-connect-app-body.gl-px-5.gl-mt-7.gl-font-base.gl-text-center
%strong= s_('Integrations|Browser limitations')
- - firefox_link_url = 'https://www.mozilla.org/en-US/firefox/'
- - firefox_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: firefox_link_url }
- = s_('Integrations|Adding a namespace works only in browsers that allow cross‑site cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable cross‑site cookies in your browser, when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe }
+ - browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
+ - firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' }
+ - chrome_link_start = browser_link_start.html_safe % { url: 'https://www.google.com/chrome/' }
+ = s_('Integrations|Adding a namespace works only in browsers that allow cross‑site cookies. Use %{firefox_link_start}Firefox%{link_end}, %{chrome_link_start}Google Chrome%{link_end}, or enable cross‑site cookies in your browser, when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, chrome_link_start: chrome_link_start, link_end: '</a>'.html_safe }
= link_to _('Learn more'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/284211', target: '_blank', rel: 'noopener noreferrer'
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 683d3a6ad1b..a89c621a55c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -44,7 +44,7 @@
- else
= stylesheet_link_tag_defer "application"
= yield :page_specific_styles
- = stylesheet_link_tag_defer "application_utilities"
+ = stylesheet_link_tag_defer 'application_utilities'
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
= stylesheet_link_tag "test_environment", media: "all" if Rails.env.test?
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index c431f05c217..e2189009045 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -1,11 +1,15 @@
= cache_if(Feature.enabled?(:cached_loading_hints, current_user), [ActionController::Base.asset_host, user_application_theme, user_color_scheme], expires_in: 1.minute) do
+ - css_crossorigin = ActionController::Base.asset_host ? 'anonymous' : nil
+
- 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' } : {} }
+ = preload_link_tag(path_to_stylesheet('application_utilities_dark'), crossorigin: css_crossorigin)
+ = preload_link_tag(path_to_stylesheet('application_dark'), crossorigin: css_crossorigin)
- 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' } : {} }
+ = preload_link_tag(path_to_stylesheet('application_utilities'), crossorigin: css_crossorigin)
+ = preload_link_tag(path_to_stylesheet('application'), crossorigin: css_crossorigin)
+ = preload_link_tag(path_to_stylesheet("highlight/themes/#{user_color_scheme}"), crossorigin: css_crossorigin)
- if Gitlab::Tracking.enabled? && Gitlab::CurrentSettings.snowplow_collector_hostname
%link{ rel: 'preconnect', href: Gitlab::CurrentSettings.snowplow_collector_hostname, crossorigin: '' }
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index 74d05be7f95..95ebe09a2e6 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -14,6 +14,7 @@
= stylesheet_link_tag 'mailer.css'
%body
+ = yield :preview_text
%table#body{ border: "0", cellpadding: "0", cellspacing: "0" }
%tbody
%tr.line
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 6c959f5e60c..ba2d6aa79eb 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -11,7 +11,6 @@
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/header/registration_enabled_callout"
- = render "layouts/header/service_templates_deprecation_callout"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/service_ping_consent"
@@ -29,5 +28,7 @@
= yield :before_content
= yield
= yield :after_content
+ -# This is needed by [GitLab JH](https://gitlab.com/gitlab-jh/jh-team/gitlab-cn/-/issues/81)
+ = render_if_exists "shared/footer/global_footer"
= render "layouts/nav/top_nav_responsive", class: 'layout-page content-wrapper-margin'
diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml
deleted file mode 100644
index 3405fb9d5ef..00000000000
--- a/app/views/layouts/_recaptcha_verification.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- humanized_resource_name = spammable.class.model_name.human.downcase
-
-%h3.page-title
- = _('Anti-spam verification')
-%hr
-
-%p
- = _("We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed.") % { humanized_resource_name: humanized_resource_name }
-
-= render 'shared/recaptcha_form', spammable: spammable
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 87580e57e75..2f6287bdfb3 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,12 +1,11 @@
- has_impersonation_link = header_link?(:admin_impersonation)
- user_status_data = user_status_properties(current_user)
-- use_top_nav_redesign = Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
.header-content
- .title-container.hide-when-menu-expanded
+ .title-container.hide-when-top-nav-responsive-open
%h1.title
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
@@ -20,24 +19,18 @@
%span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1
= _('Next')
- - if use_top_nav_redesign
- .gl-display-none.gl-sm-display-block
- = render "layouts/nav/top_nav"
- - else
- - if current_user
- = render "layouts/nav/dashboard"
- - else
- = render "layouts/nav/explore"
+ .gl-display-none.gl-sm-display-block
+ = render "layouts/nav/top_nav"
.navbar-collapse.collapse
%ul.nav.navbar-nav
- if current_user
- = render 'layouts/header/new_dropdown', class: ('gl-display-none gl-sm-display-block' if use_top_nav_redesign)
+ = render 'layouts/header/new_dropdown', class: 'gl-display-none gl-sm-display-block'
- if top_nav_show_search
- search_menu_item = top_nav_search_menu_item_attrs
%li.nav-item.d-none.d-lg-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
- %li.nav-item{ class: use_top_nav_redesign ? 'd-none d-sm-inline-block d-lg-none' : 'd-inline-block d-lg-none' }
+ %li.nav-item{ class: 'd-none d-sm-inline-block d-lg-none' }
= link_to search_menu_item.fetch(:href), title: search_menu_item.fetch(:title), aria: { label: search_menu_item.fetch(:title) }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon(search_menu_item.fetch(:icon))
- if header_link?(:issues)
@@ -118,14 +111,11 @@
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-default btn-sign-in'
- %button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: ('gl-border-none!' if use_top_nav_redesign) }
+ %button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: 'gl-border-none!', data: { testid: 'top-nav-responsive-toggle' } }
%span.sr-only= _('Toggle navigation')
- - if use_top_nav_redesign
- %span.more-icon.gl-px-3.gl-font-sm.gl-font-weight-bold
- %span.gl-pr-2= _('Menu')
- = sprite_icon('hamburger', size: 16)
- - else
- = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon')
+ %span.more-icon.gl-px-3.gl-font-sm.gl-font-weight-bold
+ %span.gl-pr-2= _('Menu')
+ = sprite_icon('hamburger', size: 16)
= sprite_icon('close', size: 12, css_class: 'close-icon')
- if display_whats_new?
diff --git a/app/views/layouts/header/_service_templates_deprecation_callout.html.haml b/app/views/layouts/header/_service_templates_deprecation_callout.html.haml
deleted file mode 100644
index 056d4426d5a..00000000000
--- a/app/views/layouts/header/_service_templates_deprecation_callout.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- return unless show_service_templates_deprecated_callout?
-
-- doc_link_start = "<a href=\"#{integrations_help_page_path}\" target='_blank' rel='noopener noreferrer'>".html_safe
-- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
-
-%div{ class: [container_class, @content_class, 'gl-pt-5!'] }
- .gl-alert.gl-alert-warning.js-service-templates-deprecated-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::SERVICE_TEMPLATES_DEPRECATED_CALLOUT, dismiss_endpoint: user_callouts_path } }
- = sprite_icon('warning', size: 16, css_class: 'gl-alert-icon')
- %button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-service-templates-deprecated-callout' } }
- = sprite_icon('close', size: 16)
- .gl-alert-title
- = s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
- .gl-alert-body
- = html_escape_once(s_('AdminSettings|You should migrate to %{doc_link_start}Project integration management%{link_end}, available at %{settings_link_start}Settings &gt; Integrations.%{link_end}')).html_safe % { doc_link_start: doc_link_start, settings_link_start: settings_link_start, link_end: '</a>'.html_safe }
- .gl-alert-actions
- = link_to admin_application_settings_services_path, class: 'btn gl-alert-action btn-info btn-md gl-button' do
- %span.gl-button-text
- = s_('AdminSettings|See affected service templates')
- = link_to "https://gitlab.com/gitlab-org/gitlab/-/issues/325905", class: 'btn gl-alert-action btn-default btn-md gl-button', target: '_blank', rel: 'noopener noreferrer' do
- %span.gl-button-text
- = _('Leave feedback')
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
deleted file mode 100644
index e4cdb4e1b08..00000000000
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ /dev/null
@@ -1,90 +0,0 @@
--# 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", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do
- %button{ type: 'button', data: { toggle: "dropdown" } }
- = _('Projects')
- = sprite_icon('chevron-down', css_class: 'caret-down')
- .dropdown-menu.frequent-items-dropdown-menu
- = render "layouts/nav/projects_dropdown/show"
-
- - if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "d-none d-md-block home dropdown header-groups", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
- %button{ type: 'button', data: { toggle: "dropdown" } }
- = _('Groups')
- = sprite_icon('chevron-down', css_class: 'caret-down')
- .dropdown-menu.frequent-items-dropdown-menu
- = render "layouts/nav/groups_dropdown/show"
-
- - if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
- = nav_link(html_options: { id: 'nav-more-dropdown', class: "header-more dropdown", data: { track_label: "more_dropdown", track_event: "click_more_link" } }) do
- %a{ href: "#", data: { toggle: "dropdown" } }
- = _('More')
- = sprite_icon('chevron-down', css_class: 'caret-down')
- .dropdown-menu
- %ul
- - if dashboard_nav_link?(:groups)
- %li.d-md-none
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups' do
- = _('Groups')
- - if dashboard_nav_link?(:activity)
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity' do
- = _('Activity')
-
- - if dashboard_nav_link?(:milestones)
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones' do
- = _('Milestones')
-
- - if dashboard_nav_link?(:snippets)
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets' do
- = _('Snippets')
-
- %li.dropdown
- = render_if_exists 'dashboard/nav_link_list'
-
- - if current_user.admin?
- = nav_link(controller: 'admin/dashboard') do
- = link_to admin_root_path, class: 'admin-icon d-xl-none' do
- = _('Admin Area')
- - if Gitlab::CurrentSettings.admin_mode
- - if header_link?(:admin_mode)
- = nav_link(controller: 'admin/sessions') do
- = link_to destroy_admin_session_path, method: :post, class: 'd-lg-none lock-open-icon' do
- = _('Leave Admin Mode')
- - elsif current_user.admin?
- = nav_link(controller: 'admin/sessions') do
- = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
- = _('Enter Admin Mode')
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, class: 'admin-icon' do
- = _('Sherlock Transactions')
-
- - if current_user.admin?
- = nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-xl-block"}) do
- = link_to admin_root_path, class: 'admin-icon', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('admin', size: 18)
-
- - if Gitlab::CurrentSettings.admin_mode
- - if header_link?(:admin_mode)
- = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block"}) do
- = link_to destroy_admin_session_path, method: :post, title: _('Leave Admin Mode'), aria: { label: _('Leave Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = sprite_icon('lock-open', size: 18)
- - elsif current_user.admin?
- = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block"}) do
- = link_to new_admin_session_path, title: _('Enter Admin Mode'), aria: { label: _('Enter Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = sprite_icon('lock', size: 18)
-
- -# Shortcut to Dashboard > Projects
- - if dashboard_nav_link?(:projects)
- %li.hidden
- = link_to dashboard_projects_path, class: 'dashboard-shortcuts-projects' do
- = _('Projects')
-
- = render_if_exists 'layouts/nav/geo_primary_node_url'
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
deleted file mode 100644
index 5b47eb27b04..00000000000
--- a/app/views/layouts/nav/_explore.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
--# 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
- = link_to explore_root_path, title: _('Projects'), class: 'dashboard-shortcuts-projects' do
- = _('Projects')
- - if explore_nav_link?(:groups)
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to explore_groups_path, title: _('Groups'), class: 'dashboard-shortcuts-groups' do
- = _('Groups')
- - if explore_nav_link?(:snippets)
- = nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: _('Snippets'), class: 'dashboard-shortcuts-snippets' do
- = _('Snippets')
- %li
- = link_to _("Help"), help_path, title: _('About GitLab CE')
diff --git a/app/views/layouts/nav/_top_nav_responsive.html.haml b/app/views/layouts/nav/_top_nav_responsive.html.haml
index 0d122f1adff..86fd8b6d80c 100644
--- a/app/views/layouts/nav/_top_nav_responsive.html.haml
+++ b/app/views/layouts/nav/_top_nav_responsive.html.haml
@@ -1,5 +1,3 @@
-- return unless Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
-
- top_class = local_assigns.fetch(:class, nil)
- view_model = top_nav_responsive_view_model(project: @project, group: @group)
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
deleted file mode 100644
index d7b0c7150d4..00000000000
--- a/app/views/layouts/nav/groups_dropdown/_show.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
--# 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.with-deprecated-styles
- .frequent-items-dropdown-sidebar
- %ul
- = nav_link(path: 'dashboard/groups#index') do
- = link_to dashboard_groups_path, data: { track_label: "groups_dropdown_your_groups", track_event: "click_link" } do
- = _('Your groups')
- = nav_link(path: 'groups#explore') do
- = link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
- = _('Explore groups')
- - if current_user.can_create_group?
- = 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
- = _('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
- = _('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
deleted file mode 100644
index f16aab92a95..00000000000
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
--# 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.with-deprecated-styles
- .frequent-items-dropdown-sidebar
- %ul
- = nav_link(path: 'dashboard/projects#index') do
- = link_to dashboard_projects_path, data: { track_label: "projects_dropdown_your_projects", track_event: "click_link" } do
- = _('Your projects')
- = nav_link(path: 'projects#starred') do
- = link_to starred_dashboard_projects_path, data: { track_label: "projects_dropdown_starred_projects", track_event: "click_link" } do
- = _('Starred projects')
- = nav_link(path: 'projects#trending') do
- = link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
- = _('Explore projects')
- = nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
- = link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", qa_selector: "create_project_link" } do
- = _('Create blank project')
- = nav_link(path: 'projects/new#import_project') do
- = link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", qa_selector: "import_project_link" } do
- = _('Import project')
- = nav_link(path: 'projects/new#create_from_template') do
- = link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do
- = _('Create from template')
- .frequent-items-dropdown-content
- #js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
diff --git a/app/views/layouts/nav/sidebar/_analytics_links.html.haml b/app/views/layouts/nav/sidebar/_analytics_links.html.haml
deleted file mode 100644
index 92a7b97203f..00000000000
--- a/app/views/layouts/nav/sidebar/_analytics_links.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-- navbar_links = links.sort_by(&:title)
-- all_paths = navbar_links.map(&:path)
-- analytics_link = navbar_links.find { |link| link.title == _('Value stream') } || navbar_links.first
-
-- if navbar_links.any?
- = nav_link(path: all_paths) do
- = link_to analytics_link.link, {class: 'shortcuts-analytics has-sub-items', data: { qa_selector: 'analytics_anchor' } } do
- .nav-icon-container
- = sprite_icon('chart')
- %span.nav-item-name{ data: { qa_selector: 'analytics_link' } }
- = _('Analytics')
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'analytics_sidebar_submenu' } }
- = nav_link(path: analytics_link.path, html_options: { class: "fly-out-top-item" } ) do
- = link_to analytics_link.link do
- %strong.fly-out-top-item-name
- = _('Analytics')
- %li.divider.fly-out-top-item
- - navbar_links.each do |menu_item|
- = nav_link(path: menu_item.path) do
- = link_to(menu_item.link, menu_item.link_to_options) do
- %span= menu_item.title
diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml
index 5738c8becd5..25b6c264d92 100644
--- a/app/views/layouts/nav/sidebar/_group_menus.html.haml
+++ b/app/views/layouts/nav/sidebar/_group_menus.html.haml
@@ -1,166 +1,3 @@
-- issues_count = cached_issuables_count(@group, type: :issues)
-- merge_requests_count = cached_issuables_count(@group, type: :merge_requests)
-
-= render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group
-
-- if group_sidebar_link?(:overview)
- - paths = group_overview_nav_link_paths
- = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do
- = link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do
- .nav-icon-container
- = sprite_icon('group')
- %span.nav-item-name
- = group_information_title(@group)
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} }
- = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do
- = link_to activity_group_path(@group) do
- %strong.fly-out-top-item-name
- = group_information_title(@group)
- %li.divider.fly-out-top-item
-
- - if group_sidebar_link?(:activity)
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: _('Activity') do
- %span
- = _('Activity')
-
- - if group_sidebar_link?(:labels)
- = nav_link(path: 'labels#index') do
- = link_to group_labels_path(@group), title: _('Labels') do
- %span
- = _('Labels')
-
- - 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)
- = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do
- = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('issues')
- %span.nav-item-name
- = _('Issues')
- %span.badge.badge-pill.count= issues_count
-
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} }
- = 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')
- %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count
-
- %li.divider.fly-out-top-item
- = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
- = link_to issues_group_path(@group), title: _('List') do
- %span
- = _('List')
-
- - if group_sidebar_link?(:boards)
- = nav_link(path: ['boards#index', 'boards#show']) do
- = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do
- %span
- = boards_link_text
-
- - if group_sidebar_link?(:milestones)
- = nav_link(path: 'milestones#index') do
- = link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do
- %span
- = _('Milestones')
-
- = render_if_exists 'layouts/nav/sidebar/group_iterations_link'
-
-- if group_sidebar_link?(:merge_requests)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group) do
- .nav-icon-container
- = sprite_icon('git-merge')
- %span.nav-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count= merge_requests_count
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Merge requests')
- %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
-
-= render_if_exists "layouts/nav/ee/security_link" # EE-specific
-
-= render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
-
-- if group_sidebar_link?(:kubernetes)
- = nav_link(controller: [:clusters]) do
- = link_to group_clusters_path(@group) do
- .nav-icon-container
- = sprite_icon('cloud-gear')
- %span.nav-item-name
- = _('Kubernetes')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do
- = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
- %strong.fly-out-top-item-name
- = _('Kubernetes')
-
-= render 'groups/sidebar/packages'
-
-= render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user)
-
-- if group_sidebar_link?(:wiki)
- = render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url
-
-- if group_sidebar_link?(:settings)
- = nav_link(path: group_settings_nav_link_paths) do
- = link_to edit_group_path(@group), class: 'has-sub-items' do
- .nav-icon-container
- = sprite_icon('settings')
- %span.nav-item-name{ data: { qa_selector: 'group_settings' } }
- = _('Settings')
- %ul.sidebar-sub-level-items{ data: { testid: 'group-settings-menu', qa_selector: 'group_sidebar_submenu' } }
- = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do
- = link_to edit_group_path(@group) do
- %strong.fly-out-top-item-name
- = _('Settings')
- %li.divider.fly-out-top-item
- = nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do
- %span
- = _('General')
-
- = nav_link(controller: :integrations) do
- = link_to group_settings_integrations_path(@group), title: _('Integrations') do
- %span
- = _('Integrations')
-
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: _('Projects') do
- %span
- = _('Projects')
-
- = nav_link(controller: :repository) do
- = link_to group_settings_repository_path(@group), title: _('Repository') do
- %span
- = _('Repository')
-
- = nav_link(controller: [:ci_cd, 'groups/runners']) do
- = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
- %span
- = _('CI/CD')
-
- = nav_link(controller: :applications) do
- = link_to group_settings_applications_path(@group), title: _('Applications') do
- %span
- = _('Applications')
-
- = render 'groups/sidebar/packages_settings'
-
- = render_if_exists "groups/ee/settings_nav"
-
= render_if_exists "groups/ee/administration_nav"
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_wiki_link.html.haml b/app/views/layouts/nav/sidebar/_wiki_link.html.haml
deleted file mode 100644
index b6b63b75fcc..00000000000
--- a/app/views/layouts/nav/sidebar/_wiki_link.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-= nav_link(controller: :wikis) do
- = link_to wiki_url, class: 'shortcuts-wiki', data: { qa_selector: 'wiki_link' } do
- .nav-icon-container
- = sprite_icon('book')
- %span.nav-item-name
- = _('Wiki')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
- = link_to wiki_url do
- %strong.fly-out-top-item-name
- = _('Wiki')
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index a4ea63e3d53..843a820bd1b 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -1,60 +1,68 @@
-- placeholders = { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_or_group_name: member_source.human_name, project_or_group: member_source.model_name.singular, br_tag: '<br/>'.html_safe, role: member.human_access.downcase }
+- placeholders = { strong_start: '<strong>'.html_safe,
+ strong_end: '</strong>'.html_safe,
+ project_or_group_name: member_source.human_name,
+ project_or_group: member_source.model_name.singular,
+ br_tag: '<br/>'.html_safe,
+ role: member.human_access.downcase }
+- join_text = s_('InviteEmail|Join now')
+- join_url = invite_url(@token, invite_type: Emails::Members::INITIAL_INVITE, experiment_name: 'invite_email_preview_text')
+- inviter_name = member.created_by.name if member.created_by
-- experiment('members/invite_email', actor: member) do |experiment_instance|
- - experiment_instance.use do
- %tr
- %td.text-content
- %h2.invite-header
- = s_('InviteEmail|You are invited!')
- %p
- - if member.created_by
- = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to member.created_by.name, user_url(member.created_by)).html_safe })
- - else
- = html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
- %p.invite-actions
- = link_to s_('InviteEmail|Join now'), invite_url(@token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE), class: 'invite-btn-join'
- - experiment_instance.try(:activity) do
- %tr
- %td.text-content{ colspan: 2 }
- %img.mail-avatar{ height: "60", src: avatar_icon_for_user(member.created_by, 60, only_path: false), width: "60", alt: "" }
- %p
- = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to member.created_by.name, user_url(member.created_by)).html_safe })
- %p.invite-actions
- = link_to s_('InviteEmail|Join now'), invite_url(@token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE), class: 'invite-btn-join'
- %tr.border-top
- %td.text-content.mailer-align-left.half-width
- %h4
- = s_('InviteEmail|%{project_or_group} details') % { project_or_group: member_source.model_name.singular.capitalize }
- %ul
- %li
- %div
- %img.mailer-icon{ alt: '', src: image_url("mailers/members/users.png") }
- %span
- - member_count = member_source.members.size
- = n_('%{bold_start}%{count}%{bold_end} member', '%{bold_start}%{count}%{bold_end} members',
- member_count).html_safe % { count: number_with_delimiter(member_count),
- bold_start: '<b>'.html_safe,
- bold_end: '</b>'.html_safe }
- %li
- %div
- %img.mailer-icon{ alt: '', src: image_url("mailers/members/issues.png") }
- %span
- - issue_count = member_source.open_issues_count(member.created_by)
- = n_('%{bold_start}%{count}%{bold_end} issue', '%{bold_start}%{count}%{bold_end} issues',
- issue_count).html_safe % { count: number_with_delimiter(issue_count),
- bold_start: '<b>'.html_safe,
- bold_end: '</b>'.html_safe }
- %li
- %div
- %img.mailer-icon{ alt: '', src: image_url("mailers/members/merge-request-open.png") }
- %span
- - mr_count = member_source.open_merge_requests_count(member.created_by)
- = n_('%{bold_start}%{count}%{bold_end} opened merge request', '%{bold_start}%{count}%{bold_end} opened merge requests',
- mr_count).html_safe % { count: number_with_delimiter(mr_count),
- bold_start: '<b>'.html_safe,
- bold_end: '</b>'.html_safe }
- %td.text-content.mailer-align-left.half-width
- %h4
- = s_("InviteEmail|What's it about?")
- %p
- = invited_to_description(member_source)
+- experiment(:invite_email_preview_text, actor: member) do |experiment_instance|
+ - experiment_instance.use {}
+ - experiment_instance.candidate do
+ = content_for :preview_text do
+ %div{ style: "display:none;font-size:1px;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;" }
+ - if member.created_by
+ = s_('InviteEmail|Join your team on GitLab! %{inviter} invited you to %{project_or_group_name}') % { inviter: inviter_name, project_or_group_name: placeholders[:project_or_group_name] }
+ - else
+ = s_('InviteEmail|Join your team on GitLab! You are invited to %{project_or_group_name}') % { project_or_group_name: placeholders[:project_or_group_name] }
+ = gmail_goto_action(join_text, join_url)
+
+%tr
+ %td.text-content{ colspan: 2 }
+ %img.mail-avatar{ height: "60", src: avatar_icon_for_user(member.created_by, 60, only_path: false), width: "60", alt: "" }
+ %p
+ - if member.created_by
+ = html_escape(s_("InviteEmail|%{inviter} invited you to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders.merge({ inviter: (link_to inviter_name, user_url(member.created_by)).html_safe })
+ - else
+ = html_escape(s_("InviteEmail|You are invited to join the %{strong_start}%{project_or_group_name}%{strong_end}%{br_tag}%{project_or_group} as a %{role}")) % placeholders
+ %p.invite-actions
+ = link_to join_text, join_url, class: 'invite-btn-join'
+%tr.border-top
+ %td.text-content.mailer-align-left.half-width
+ %h4
+ = s_('InviteEmail|%{project_or_group} details') % { project_or_group: member_source.model_name.singular.capitalize }
+ %ul
+ %li
+ %div
+ %img.mailer-icon{ alt: '', src: image_url("mailers/members/users.png") }
+ %span
+ - member_count = member_source.members.size
+ = n_('%{bold_start}%{count}%{bold_end} member', '%{bold_start}%{count}%{bold_end} members',
+ member_count).html_safe % { count: number_with_delimiter(member_count),
+ bold_start: '<b>'.html_safe,
+ bold_end: '</b>'.html_safe }
+ %li
+ %div
+ %img.mailer-icon{ alt: '', src: image_url("mailers/members/issues.png") }
+ %span
+ - issue_count = member_source.open_issues_count(member.created_by)
+ = n_('%{bold_start}%{count}%{bold_end} issue', '%{bold_start}%{count}%{bold_end} issues',
+ issue_count).html_safe % { count: number_with_delimiter(issue_count),
+ bold_start: '<b>'.html_safe,
+ bold_end: '</b>'.html_safe }
+ %li
+ %div
+ %img.mailer-icon{ alt: '', src: image_url("mailers/members/merge-request-open.png") }
+ %span
+ - mr_count = member_source.open_merge_requests_count(member.created_by)
+ = n_('%{bold_start}%{count}%{bold_end} opened merge request', '%{bold_start}%{count}%{bold_end} opened merge requests',
+ mr_count).html_safe % { count: number_with_delimiter(mr_count),
+ bold_start: '<b>'.html_safe,
+ bold_end: '</b>'.html_safe }
+ %td.text-content.mailer-align-left.half-width
+ %h4
+ = s_("InviteEmail|What's it about?")
+ %p
+ = invited_to_description(member_source)
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 0328fa5c282..6eba0309a4f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -77,12 +77,10 @@
.row.user-time-preferences.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_("Profiles|Time settings")
- %p= s_("Profiles|You can set your current timezone here")
+ %p= s_("Profiles|Set your local time zone")
.col-lg-8
- -# TODO: might need an entry in user/profile.md to describe some of these settings
- -# https://gitlab.com/gitlab-org/gitlab-foss/issues/60070
%h5= _("Time zone")
- = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
+ = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
.col-lg-12
%hr
@@ -100,6 +98,7 @@
= f.text_field :id, class: 'gl-form-input', readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
= f.text_field :pronouns, class: 'input-md gl-form-input', help: s_("Profiles|Enter your pronouns to let people know how to refer to you")
+ = f.text_field :pronunciation, class: 'input-md gl-form-input', help: s_("Profiles|Enter how your name is pronounced to help people address you correctly")
= render_if_exists 'profiles/email_settings', form: f
= f.text_field :skype, class: 'input-md gl-form-input', placeholder: s_("Profiles|username")
= f.text_field :linkedin, class: 'input-md gl-form-input', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename")
@@ -123,10 +122,9 @@
= f.check_box :include_private_contributions, label: s_('Profiles|Include private contributions on my profile'), wrapper_class: 'mb-2', inline: true
.help-block
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
- .row.gl-justify-content-end.gl-mt-5
- .col-lg-8.gl-display-flex
- = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3'
- = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
+ %hr
+ = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3'
+ = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
.modal.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } }
.modal-dialog
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 86172499118..8909536a1ec 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,5 @@
- empty_repo = @project.empty_repo?
- 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)
@@ -25,24 +24,8 @@
%span.access-request-links.gl-ml-3
= render 'shared/members/access_request_links', source: @project
- - if @project.topic_list.present?
- = cache_if(cache_enabled, [@project, :topic_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')
-
- - @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(topic: 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 }
+ .gl-mt-3.gl-pl-3.gl-w-full
+ = render "shared/projects/topics", project: @project, cache_enabled: cache_enabled
= 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
@@ -88,12 +71,11 @@
= render_if_exists "projects/home_mirror"
- if @project.badges.present?
- = 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' }>
+ .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/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index 5dd6ec0addf..6375a56bf5d 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -1,8 +1,5 @@
-- return unless can_import_members?
+- return unless can_admin_project_member?(project)
-.js-invite-members-modal{ data: { id: project.id,
- name: project.name,
- is_project: 'true',
+.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.access_level_roles.to_json,
- default_access_level: Gitlab::Access::GUEST,
- help_link: help_page_url('user/permissions') } }
+ help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index afe42b334ba..9ba7d25b662 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,19 +1,21 @@
- event = last_push_event
- if event && show_last_push_widget?(event)
- .row-content-block.top-block.d-none.d-sm-block.white
- .event-last-push
- .event-last-push-text
- %span= s_("LastPushEvent|You pushed to")
- %strong
- = link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name'
+ .gl-alert.gl-alert-success.mt-2{ role: 'alert' }
+ = sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ .gl-alert-body
+ %span= s_("LastPushEvent|You pushed to")
+ %strong.gl-display-inline-flex.gl-max-w-50p{ data: { toggle: 'tooltip' }, title: event.ref_name }
+ = link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name gl-text-truncate'
- - if event.project != @project
- %span= s_("LastPushEvent|at")
- %strong= link_to_project event.project
+ - if event.project != @project
+ %span= s_("LastPushEvent|at")
+ %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
+ #{time_ago_with_tooltip(event.created_at)}
- - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
- .flex-right
- = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn gl-button btn-info btn-sm qa-create-merge-request" do
- #{ _('Create merge request') }
+ - if can?(current_user, :create_merge_request_in, event.project.default_merge_request_target)
+ .gl-alert-actions
+ = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn gl-button btn-confirm qa-create-merge-request" do
+ #{ _('Create merge request') }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 55696337bc1..026c7a0d79d 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -54,8 +54,7 @@
.form-group.row.initialize-with-readme-setting
%div{ :class => "col-sm-12" }
.form-check
- - experiment(:new_project_readme, actor: current_user) do |e|
- = check_box_tag 'project[initialize_with_readme]', '1', e.run, class: 'form-check-input', data: { qa_selector: "initialize_with_readme_checkbox", track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme", track_value: "" }
+ = check_box_tag 'project[initialize_with_readme]', '1', true, class: 'form-check-input', data: { qa_selector: "initialize_with_readme_checkbox", track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme", track_value: "" }
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
%strong= s_('ProjectsNew|Initialize repository with a README')
diff --git a/app/views/projects/_terraform_banner.html.haml b/app/views/projects/_terraform_banner.html.haml
index a30c4a2d624..881e4ccd9df 100644
--- a/app/views/projects/_terraform_banner.html.haml
+++ b/app/views/projects/_terraform_banner.html.haml
@@ -2,4 +2,4 @@
- if show_terraform_banner?(project)
.container-fluid{ class: @content_class }
- .js-terraform-notification{ data: { project_id: project.id } }
+ .js-terraform-notification{ data: { terraform_image_path: image_path('illustrations/third-party-logos/ci_cd-template-logos/terraform.svg') } }
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8fe9c9e5c52..30c052e054f 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -51,7 +51,7 @@
= i
\
- %td.lines
+ %td.lines.gl-w-full
%pre.code.highlight
%code
- blame_group[:lines].each do |line|
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index f2f753b4e86..41333c416de 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -5,7 +5,7 @@
.file-holder-bottom-radius.file-holder.file.gl-mb-3
.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)
+ = sprite_icon('branch', size: 12)
= ref
- if current_action?(:edit) || current_action?(:update)
%span.float-left.gl-mr-3
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
index d8d27c3330b..afde59aa37d 100644
--- a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml
@@ -6,4 +6,4 @@
= s_('Pipelines|This GitLab CI configuration is invalid:')
= viewer.validation_message(project: @project, sha: @commit.sha, user: @current_user)
-= link_to _('Learn more'), help_page_path('ci/yaml/README')
+= link_to _('Learn more'), help_page_path('ci/yaml/index')
diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
index b1c8e110493..cf57f1b531d 100644
--- a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
+++ b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml
@@ -1,4 +1,4 @@
= loading_icon(css_class: "gl-vertical-align-text-bottom mr-1")
= s_('Pipelines|Validating GitLab CI configuration…')
-= link_to _('Learn more'), help_page_path('ci/yaml/README')
+= link_to _('Learn more'), help_page_path('ci/yaml/index')
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index 068cf400cd5..30182c100d5 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'going-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index 8610847fbc9..c48ab84654f 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= loading_icon(css_class: "gl-vertical-align-text-bottom gl-mr-1")
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'going-from-source-files-to-public-pages')
+= link_to 'Learn more', help_page_path('ci/environments/index.md', anchor: 'go-from-source-files-to-public-pages')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 60cb06f71ba..99a9535b8e8 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -4,7 +4,7 @@
%li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } }
.branch-info
.branch-title
- = sprite_icon('fork', size: 12, css_class: 'gl-flex-shrink-0')
+ = sprite_icon('branch', size: 12, css_class: 'gl-flex-shrink-0')
= link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do
= branch.name
- if branch.name == @repository.root_ref
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 824e876500b..1a3813ba99f 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -12,12 +12,15 @@
%td.status
= render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
- %td.branch-commit.cgray
+ %td
- if can?(current_user, :read_build, job)
- = link_to project_job_path(job.project, job) do
- %span.build-link ##{job.id}
+ = link_to job.name, project_job_path(job.project, job), class: 'gl-text-blue-600!'
- else
- %span.build-link ##{job.id}
+ %span.gl-text-blue-600!
+ = job.name
+
+ %td.branch-commit.gl-text-gray-900
+ %span.build-link ##{job.id}
- if ref
- if job.ref
@@ -79,9 +82,6 @@
= job.stage
%td
- = job.name
-
- %td
- if job.duration
%p.duration
= custom_icon("icon_timer")
@@ -105,7 +105,7 @@
- if can?(current_user, :update_build, job)
- if job.active?
= link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'gl-button btn btn-default btn-icon' do
- = sprite_icon('close', css_class: 'gl-icon')
+ = sprite_icon('cancel', css_class: 'gl-icon')
- elsif job.scheduled?
.gl-button.btn.btn-default.btn-icon.disabled{ disabled: true }
= sprite_icon('planning', css_class: 'gl-icon')
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 67007aa7448..36d3520cb59 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -57,7 +57,7 @@
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) }
.mr-widget-pipeline-graph
.stage-cell
- .js-commit-pipeline-mini-graph{ data: { stages: @last_pipeline_stages.to_json.html_safe } }
+ .js-commit-pipeline-mini-graph{ data: { stages: @last_pipeline_stages.to_json.html_safe, full_path: @project.full_path, iid: @last_pipeline.iid } }
- if @last_pipeline.duration
in
= time_interval_in_words @last_pipeline.duration
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index c3fdfeb6f4e..e22d33e3c72 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,7 +9,7 @@
- page_description @commit.description
- add_page_specific_style 'page_bundles/pipelines'
-.container-fluid{ class: [limited_container_width, container_class] }
+.container-fluid.commits-container{ class: [limited_container_width, container_class] }
= render "commit_box"
= render "ci_menu"
= render "projects/diffs/diffs",
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 463984a13a2..22a5bada311 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -26,7 +26,7 @@
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control gl-form-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false }
.control.d-none.d-md-block
- = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-default btn-icon' do
+ = link_to project_commits_path(@project, @id, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-default btn-icon' do
= sprite_icon('rss', css_class: 'qa-rss-icon')
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/deployments/_confirm_rollback_modal.html.haml b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
deleted file mode 100644
index 23729d6ebf9..00000000000
--- a/app/views/projects/deployments/_confirm_rollback_modal.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-- commit_sha = link_to deployment.short_sha, project_commit_path(@project, deployment.sha), class: "commit-sha has-tooltip", title: h(deployment.commit_title)
-.modal.ws-normal.fade{ tabindex: -1, id: "confirm-rollback-modal-#{deployment.id}" }
- .modal-dialog
- .modal-content
- .modal-header
- %h4.modal-title.d-flex.mw-100
- - if deployment.last?
- = s_("Environments|Re-deploy environment %{environment_name}?") % {environment_name: @environment.name}
- - else
- = s_("Environments|Rollback environment %{environment_name}?") % {environment_name: @environment.name}
- .modal-body
- - if deployment.last?
- %p= s_('Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
- - else
- %p
- = s_('Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha, environment_name: @environment.name}
- .modal-footer
- = button_tag _('Cancel'), type: 'button', class: 'btn gl-button btn-danger', data: { dismiss: 'modal' }
- = link_to [:retry, @project, deployment.deployable], method: :post, class: 'btn gl-button btn-danger' do
- - if deployment.last?
- = s_('Environments|Re-deploy')
- - else
- = s_('Environments|Rollback')
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index 78972a5b7b9..a7befabdc96 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,8 +1,7 @@
- if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
- = button_tag class: 'gl-button btn btn-default btn-icon has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
+ = button_tag class: 'js-confirm-rollback-modal-button gl-button btn btn-default btn-icon has-tooltip', type: 'button', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) }, title: tooltip do
- if deployment.last?
= sprite_icon('repeat', css_class: 'gl-icon')
- else
= sprite_icon('redo', css_class: 'gl-icon')
- = render 'projects/deployments/confirm_rollback_modal', deployment: deployment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 35e2fe1b398..418a65118f5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -16,7 +16,7 @@
- unless diff_file.submodule?
.file-actions.gl-display-none.gl-sm-display-flex
- if diff_file.blob&.readable_text?
- %span.has-tooltip.gl-mr-3{ title: _("Toggle comments for this file") }
+ %span.has-tooltip{ title: _("Toggle comments for this file") }
= link_to '#', class: 'js-toggle-diff-comments btn gl-button btn-default btn-icon selected', disabled: @diff_notes_disabled do
= sprite_icon('comment')
\
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 187fe608a68..926a0610577 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -19,7 +19,7 @@
.settings-content
= form_for @project, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
- %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
+ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
.js-project-permissions-form
- if show_visibility_confirm_modal?(@project)
= render "visibility_modal"
@@ -54,6 +54,8 @@
.settings-content
= render 'shared/badges/badge_settings'
+= render_if_exists 'compliance_management/compliance_framework/project_settings', expanded: expanded
+
= render_if_exists 'projects/settings/default_issue_template'
= render 'projects/service_desk_settings'
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 70d86e79282..905794c0730 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -7,7 +7,7 @@
= render "home_panel"
= render "archived_notice", project: @project
-= render 'invite_members_empty_project' if can_import_members?
+= render "invite_members_empty_project" if can_admin_project_member?(@project)
%h4.gl-mt-0.gl-mb-3
= _('The repository for this project is empty')
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
deleted file mode 100644
index b9208969fb3..00000000000
--- a/app/views/projects/environments/_external_url.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn external-url has-tooltip qa-view-deployment', title: s_('Environments|Open live environment') do
- = sprite_icon('external-link')
- = _("View deployment")
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
deleted file mode 100644
index a295c8f6fb0..00000000000
--- a/app/views/projects/environments/_form.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.row.gl-mt-3.gl-mb-3
- .col-lg-3
- %h4.gl-mt-0
- = _("Environments")
- %p
- - link_to_read_more = link_to(_("More information"), help_page_path("ci/environments/index.md"))
- = _("Environments allow you to track deployments of your application %{link_to_read_more}.").html_safe % { link_to_read_more: link_to_read_more }
-
- = form_for [@project, @environment], html: { class: 'col-lg-9' } do |f|
- = form_errors(@environment)
-
- .form-group
- = f.label :name, _('Name'), class: 'label-bold'
- = f.text_field :name, required: true, class: 'form-control'
- .form-group
- = f.label :external_url, _('External URL'), class: 'label-bold'
- = f.url_field :external_url, class: 'form-control'
-
- .form-actions
- = f.submit _('Save'), class: 'gl-button btn btn-confirm'
- = link_to _('Cancel'), project_environments_path(@project), class: 'gl-button btn btn-cancel'
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
deleted file mode 100644
index 65abaf44082..00000000000
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- environment = local_assigns.fetch(:environment)
-
-- return unless can?(current_user, :read_environment, environment)
-
-= link_to environment_metrics_path(environment), title: _('See metrics'), class: 'gl-button btn metrics-button' do
- = sprite_icon('chart', css_class: 'gl-mr-2')
- = _("Monitoring")
diff --git a/app/views/projects/environments/_pin_button.html.haml b/app/views/projects/environments/_pin_button.html.haml
deleted file mode 100644
index ec3e7e20365..00000000000
--- a/app/views/projects/environments/_pin_button.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if environment.auto_stop_at? && environment.available?
- = button_to cancel_auto_stop_project_environment_path(environment.project, environment), class: 'gl-button btn btn-secondary has-tooltip', title: _('Prevent environment from auto-stopping') do
- = sprite_icon('thumbtack')
diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml
deleted file mode 100644
index ab3363bbb07..00000000000
--- a/app/views/projects/environments/_terminal_button.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- if environment.has_terminals? && can?(current_user, :admin_environment, @project)
- = link_to terminal_project_environment_path(@project, environment), class: 'gl-button btn terminal-button' do
- = sprite_icon('terminal')
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
index c4e2c1eb63d..dcd5fb2574e 100644
--- a/app/views/projects/environments/edit.html.haml
+++ b/app/views/projects/environments/edit.html.haml
@@ -1,7 +1,6 @@
- page_title _("Edit"), @environment.name, _("Environments")
- add_page_specific_style 'page_bundles/environments'
-%h3.page-title
- = _('Edit environment')
-%hr
-= render 'form'
+#js-edit-environment{ data: { project_environments_path: project_environments_path(@project),
+ update_environment_path: project_environment_path(@project, @environment),
+ environment: environment_data(@environment)} }
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index 6b0ccc1dcc7..de7f976717b 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -2,7 +2,4 @@
- page_title _("New Environment")
- add_page_specific_style 'page_bundles/environments'
-%h3.page-title
- = _("New environment")
-%hr
-= render 'form'
+#js-new-environment{ data: { project_environments_path: project_environments_path(@project) } }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index b3e4b7a4998..b123b81b89c 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,58 +5,8 @@
- add_page_specific_style 'page_bundles/environments'
- add_page_specific_style 'page_bundles/ci_status'
-#environments-detail-view{ data: { name: @environment.name, id: @environment.id, delete_path: environment_delete_path(@environment)} }
- - if @environment.available? && can?(current_user, :stop_environment, @environment)
- #stop-environment-modal.modal.fade{ tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h4.modal-title.d-flex.mw-100
- = s_("Environments|Stopping")
- %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } }
- #{@environment.name}?
- .modal-body
- %p= s_('Environments|Are you sure you want to stop this environment?')
- - unless @environment.stop_action_available?
- .warning_message
- %p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe,
- emphasis_end: '</strong>'.html_safe,
- ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe,
- ci_config_link_end: '</a>'.html_safe }
- %a{ href: 'https://docs.gitlab.com/ee/ci/environments/index.html#stopping-an-environment',
- target: '_blank',
- rel: 'noopener noreferrer' }
- = s_('Environments|Learn more about stopping environments')
- .modal-footer
- = button_tag _('Cancel'), type: 'button', class: 'gl-button btn btn-cancel', data: { dismiss: 'modal' }
- = button_to stop_project_environment_path(@project, @environment), class: 'gl-button btn btn-danger has-tooltip', method: :post do
- = s_('Environments|Stop environment')
-
- - if can_destroy_environment?(@environment)
- #delete-environment-modal
-
- .top-area.justify-content-between
- .d-flex
- %h3.page-title= @environment.name
- - if @environment.auto_stop_at?
- %p.align-self-end.gl-ml-3
- = s_('Environments|Auto stops %{auto_stop_time}').html_safe % {auto_stop_time: time_ago_with_tooltip(@environment.auto_stop_at)}
- .nav-controls.my-2
- = render 'projects/environments/pin_button', environment: @environment
- = render 'projects/environments/terminal_button', environment: @environment
- = render 'projects/environments/external_url', environment: @environment
- = render 'projects/environments/metrics_button', environment: @environment
- - if can?(current_user, :update_environment, @environment)
- = link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- - if @environment.available? && can?(current_user, :stop_environment, @environment)
- = button_tag class: 'gl-button btn btn-danger', type: 'button', data: { toggle: 'modal',
- target: '#stop-environment-modal' } do
- = sprite_icon('stop')
- = s_('Environments|Stop')
- - if can_destroy_environment?(@environment)
- = button_tag class: 'gl-button btn btn-danger', type: 'button', data: { toggle: 'modal',
- target: '#delete-environment-modal' } do
- = s_('Environments|Delete')
+#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
+ #environments-detail-view-header
.environments-container
- if @deployments.blank?
diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml
index f71c97c2a8f..c1c9f58265d 100644
--- a/app/views/projects/feature_flags/edit.html.haml
+++ b/app/views/projects/feature_flags/edit.html.haml
@@ -9,5 +9,5 @@
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json),
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
+ environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-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 bc52f52ecf7..097475d2928 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/index.md', anchor: 'scoping-environments-with-specs'),
+ environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-environments-with-specs'),
project_id: @project.id } }
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 9c01d93f7d0..ee3aaee6dbb 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -12,6 +12,9 @@
- if issue.confidential?
%span.has-tooltip{ title: _('Confidential') }
= confidential_icon(issue)
+ - if Feature.enabled?(:ban_user_feature_flag) && issue.hidden?
+ %span.has-tooltip{ title: _('This issue is hidden because its author has been banned') }
+ = hidden_issue_icon(issue)
= link_to issue.title, issue_path(issue)
= render_if_exists 'projects/issues/subepic_flag', issue: issue
- if issue.tasks?
@@ -36,7 +39,7 @@
= sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= issue.milestone.title
- if issue.due_date
- %span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
+ %span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue? && !issue.closed?}", title: _('Due date') }
&nbsp;
= sprite_icon('calendar')
= issue.due_date.to_s(:medium)
diff --git a/app/views/projects/issues/captcha_check.html.haml b/app/views/projects/issues/captcha_check.html.haml
new file mode 100644
index 00000000000..657c5c2cd8f
--- /dev/null
+++ b/app/views/projects/issues/captcha_check.html.haml
@@ -0,0 +1,7 @@
+= render layout: 'shared/captcha_check', locals: { spammable: @issue } do
+ -# These fields are for values which are passed via URL parameters, and not included in the
+ -# issue's params, so they must be yielded to the block to be rendered.
+ -# If these are removed and no longer passed via URL parameters, the support
+ -# for yielding in the layout can also be removed.
+ = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
+ = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
deleted file mode 100644
index 935a3493a37..00000000000
--- a/app/views/projects/issues/verify.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue } do
- = hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
- = hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml
index 2d8b7315a29..cd59eae1fb7 100644
--- a/app/views/projects/jobs/_table.html.haml
+++ b/app/views/projects/jobs/_table.html.haml
@@ -21,13 +21,13 @@
%thead
%tr
%th Status
+ %th Name
%th Job
%th Pipeline
- if admin
%th Project
%th Runner
%th Stage
- %th Name
%th Duration
%th Coverage
%th
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b70bc740175..3e2c5f088f7 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -33,7 +33,7 @@
%span.project-ref-path.has-tooltip{ title: _('Target branch') }
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
- = sprite_icon('fork', size: 12, css_class: 'fork-sprite')
+ = sprite_icon('branch', size: 12, css_class: 'fork-sprite')
= merge_request.target_branch
- if merge_request.labels.any?
&nbsp;
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 7e1ca19d9b6..4aca13ae74a 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -40,7 +40,7 @@
#diff-notes-app.tab-content
#new.commits.tab-pane.active
= render "projects/merge_requests/commits"
- #diffs.diffs.tab-pane
+ #diffs.diffs.tab-pane{ class: "gl-m-0!" }
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index c4ee522bfa7..6d1ba9e693b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -97,4 +97,5 @@
#js-review-bar
= render 'projects/invite_members_modal', project: @project
-
+- if Gitlab::CurrentSettings.gitpod_enabled && !current_user&.gitpod_enabled
+ = render 'shared/gitpod/enable_gitpod_modal'
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 5a1e263141d..b89aa9d402e 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -70,10 +70,10 @@
= render 'projects/mirrors/disabled_mirror_badge'
- if mirror.last_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
- %td
+ %td.gl-display-flex
- if mirror_settings_enabled
- .btn-group.mirror-actions-group.float-right{ role: 'group' }
+ %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger.gl-mr-3{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
+ .btn-group.mirror-actions-group{ role: 'group' }
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
- %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 04f44f4748e..b3e0f71bf19 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -11,5 +11,5 @@
= check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input'
= label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label'
.form-text.text-muted
- = _('By default, if any ref (branch, tag, or commit) on the remote mirror has diverged from the local repository, the entire push will fail, and nothing will be updated. Choose this option to override this behavior. After the mirror is created, this can only be modified via the API.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs'), target: '_blank', rel: 'noopener noreferrer'
+ - link_opening_tag = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe
+ = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: '</a>'.html_safe }
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
index 76eb22109a4..42c31b3272f 100644
--- a/app/views/projects/packages/packages/show.html.haml
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -6,7 +6,7 @@
.row
.col-12
- - if Feature.enabled?(:package_details_apollo)
- #js-vue-packages-detail-new{ data: package_details_data(@project) }
+ - if Feature.enabled?(:package_details_apollo, default_enabled: :yaml)
+ #js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
- else
- #js-vue-packages-detail{ data: package_details_data(@project, @package) }
+ #js-vue-packages-detail{ data: package_details_data(@project, @package, true) }
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 1c134d914e9..30ebe4f20b6 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -5,9 +5,7 @@
- add_page_specific_style 'page_bundles/pipeline'
- add_page_specific_style 'page_bundles/reports'
- add_page_specific_style 'page_bundles/ci_status'
-
-- if Feature.enabled?(:graphql_pipeline_details, @project, default_enabled: :yaml) || Feature.enabled?(:graphql_pipeline_details_users, @current_user, default_enabled: :yaml)
- - add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
+- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
.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) } }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index d1b6db95392..0239e408e87 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,7 +1,6 @@
- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
-.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
- if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project)
@@ -11,21 +10,21 @@
%h4
= _("Project members")
.gl-justify-content-bottom.gl-display-flex.align-items-center
- - if can_manage_project_members?(@project)
+ - if can?(current_user, :admin_project_member, @project)
%p= share_project_description(@project)
- else
%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 }
.col-md-12.col-lg-6
.gl-display-flex.gl-flex-wrap.gl-justify-content-end
- - if can_import_members?
+ - if can_admin_project_member?(@project)
= 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 can_manage_project_members?(@project) && !membership_locked?
+ - if can_admin_project_member?(@project)
.js-invite-members-trigger{ data: { variant: 'success',
classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3',
trigger_source: 'project-members-page',
@@ -36,13 +35,13 @@
- if project_can_be_shared?
%h4
= _("Project members")
- - if can_manage_project_members?(@project)
+ - if can?(current_user, :admin_project_member, @project)
%p= share_project_description(@project)
- else
%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 Feature.disabled?(:invite_members_group_modal, @project.group) && can_manage_project_members?(@project) && project_can_be_shared?
+ - if Feature.disabled?(:invite_members_group_modal, @project.group) && can?(current_user, :admin_project_member, @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' }
@@ -52,13 +51,37 @@
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
+ = render 'shared/members/invite_member',
+ submit_url: project_project_members_path(@project),
+ access_levels: ProjectMember.access_level_roles,
+ default_access_level: @project_member.access_level,
+ can_import_members?: can_admin_project_member?(@project),
+ import_path: import_project_project_members_path(@project)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
- = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access', groups_select_tag_data: { skip_groups: @skip_groups }
+ = render 'shared/members/invite_group',
+ submit_url: project_group_links_path(@project),
+ access_levels: ProjectGroupLink.access_options,
+ default_access_level: ProjectGroupLink.default_access,
+ group_link_field: 'link_group_id',
+ group_access_field: 'link_group_access',
+ groups_select_tag_data: { skip_groups: @skip_groups }
- elsif !membership_locked?
- .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
+ .invite-member
+ = render 'shared/members/invite_member',
+ submit_url: project_project_members_path(@project),
+ access_levels: ProjectMember.access_level_roles,
+ default_access_level: @project_member.access_level,
+ can_import_members?: can_admin_project_member?(@project),
+ import_path: import_project_project_members_path(@project)
- elsif @project.allowed_to_share_with_group?
- .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access', groups_select_tag_data: { skip_groups: @skip_groups }
+ .invite-group
+ = render 'shared/members/invite_group',
+ access_levels: ProjectGroupLink.access_options,
+ default_access_level: ProjectGroupLink.default_access,
+ submit_url: project_group_links_path(@project),
+ group_link_field: 'link_group_id',
+ group_access_field: 'link_group_access',
+ groups_select_tag_data: { skip_groups: @skip_groups }
.js-project-members-list-app{ data: { members_data: project_members_app_data_json(@project,
members: @project_members,
group_links: @group_links,
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index eb376ff7960..5e999b7afb3 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -4,10 +4,6 @@
.bs-callout.help-callout
- if valid_runner_registrars.include?('project')
= _('These runners are specific to this project.')
- %hr
- = render partial: 'ci/runner/how_to_setup_runner_automatically',
- locals: { type: s_('Runners|specific'),
- clusters_path: project_clusters_path(@project) }
- if params[:ci_runner_templates]
%hr
= render partial: 'ci/runner/setup_runner_in_aws',
diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml
index d4a85893fa4..e8ac572df1d 100644
--- a/app/views/projects/security/configuration/show.html.haml
+++ b/app/views/projects/security/configuration/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration")
-- redesign_enabled = ::Feature.enabled?(:security_configuration_redesign, @project, default_enabled: :yaml)
-- @content_class = "limit-container-width" unless fluid_layout || !redesign_enabled
+- @content_class = "limit-container-width" unless fluid_layout
#js-security-configuration-static{ data: { project_path: @project.full_path, upgrade_path: security_upgrade_path } }
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 0891e3e0526..b185f45d129 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -19,8 +19,6 @@
= f.text_field :topics, value: @project.topic_list.join(', '), maxlength: 2000, class: "form-control gl-form-input"
%p.form-text.text-muted= _('Separate topics with commas.')
- = render_if_exists 'compliance_management/compliance_framework/project_settings', f: f
-
.row
.form-group.col-md-9
= f.label :description, _('Project description (optional)'), class: 'label-bold'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4757f50739b..e515f1e7320 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -19,6 +19,7 @@
= render "archived_notice", project: @project
= render_if_exists "projects/marked_for_deletion_notice", project: @project
= render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
+= render_if_exists 'projects/sast_entry_points', project: @project
- view_path = @project.default_view
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index a296394a2e0..8ef53c40b11 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index fb2825ad15e..2f13d7d96c7 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -1,5 +1,5 @@
-.blob-result{ data: { qa_selector: 'result_item_content' } }
- .file-holder
+.blob-result.gl-mt-3.gl-mb-5{ data: { qa_selector: 'result_item_content' } }
+ .file-holder.file-holder-top-border
.js-file-title.file-title{ data: { qa_selector: 'file_title_content' } }
= link_to blob_link, data: {track_event: 'click_text', track_label: 'blob_path', track_property: 'search_result'} do
= sprite_icon('document')
@@ -7,5 +7,12 @@
= search_blob_title(project, path)
= copy_file_path_button(path)
- if blob.data
- .file-content.code.term{ data: { qa_selector: 'file_text_content' } }
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link, highlight_line: blob.highlight_line
+ - if blob.data.size > 0
+ .file-content.code.term{ data: { qa_selector: 'file_text_content' } }
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link, highlight_line: blob.highlight_line
+ - else
+ .file-content.code
+ .nothing-here-block
+ .gl-text-gray-600.gl-font-sm
+ - max_file_size_indexed = Gitlab::CurrentSettings.elasticsearch_indexed_file_size_limit_kb.kilobytes
+ = _('The file could not be displayed because it is empty or larger than the maximum file size indexed (%{size}).') % { size: number_to_human_size(max_file_size_indexed) }
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index 551f5c048bc..63524bbf00e 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -12,7 +12,7 @@
.description.term.gl-px-0
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- - if Feature.enabled?(:search_sort_issues_by_popularity) && issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
+ - if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
%li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= issuable.upvotes_count
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index d54310bfa82..ab5ca0cd90f 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -5,10 +5,14 @@
= hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
+- group_attributes = @group&.attributes&.slice('id', 'name')&.merge(full_name: @group&.full_name)
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
- if @search_results
- - page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
+ - if @without_count
+ - page_description(_("%{scope} results for term '%{term}'") % { scope: @scope, term: @search_term })
+ - else
+ - page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
- page_card_attributes("Namespace" => @group&.full_path, "Project" => @project&.full_path)
.page-title-holder.d-flex.flex-wrap.justify-content-between
@@ -16,7 +20,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3
- #js-search-topbar{ data: { "group-initial-data": @group.to_json, "project-initial-data": project_attributes.to_json } }
+ #js-search-topbar{ data: { "group-initial-data": group_attributes.to_json, "project-initial-data": project_attributes.to_json } }
- if @search_term
= render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index 93868f13e58..ca09fd39dc1 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' : ''
-.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)')
+= form.gitlab_ui_checkbox_component :request_access_enabled,
+ _('Allow users to request access (if visibility is public or internal)'),
+ label_options: { class: label_class },
+ checkbox_options: { data: { qa_selector: 'request_access_checkbox' } }
diff --git a/app/views/shared/_captcha_check.html.haml b/app/views/shared/_captcha_check.html.haml
new file mode 100644
index 00000000000..3d611c22491
--- /dev/null
+++ b/app/views/shared/_captcha_check.html.haml
@@ -0,0 +1,37 @@
+- resource_name = spammable.class.model_name.singular
+- humanized_resource_name = spammable.class.model_name.human.downcase
+- script = local_assigns.fetch(:script, true)
+- method = params[:action] == 'create' ? :post : :put
+
+%h3.page-title
+ = _('Anti-spam verification')
+%hr
+
+%p
+ = _("We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed.") % { humanized_resource_name: humanized_resource_name }
+
+= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
+ .recaptcha
+ -# Create a hidden field for each param of the resource
+ - params[resource_name].each do |field, value|
+ = hidden_field(resource_name, field, value: value)
+
+ -# The reCAPTCHA response value will be returned in the 'g-recaptcha-response' field in non-test environments
+ = recaptcha_tags script: script, callback: 'recaptchaDialogCallback', nonce: content_security_policy_nonce unless Rails.env.test?
+
+ -# Fake the 'g-recaptcha-response' field in the test environment, so that the feature spec
+ -# can get to the (mocked) SpamVerdictService check.
+ = hidden_field_tag('g-recaptcha-response', 'abc123') if Rails.env.test?
+
+ -# Create a hidden field to pass back the ID of the spam_log record which was previously created
+ = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
+
+ -# Yields a block with given extra params which are not included in `params[resource_name]`.
+ -# Currently, this is only used for these params which are passed via URL parameters,
+ -# and can be removed once they are no longer needed to be passed:
+ -# - merge_request_to_resolve_discussions_of
+ -# - discussion_to_resolve
+ = yield
+
+ .row-content-block.footer-block
+ = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm'
diff --git a/app/views/shared/_check_recovery_settings.html.haml b/app/views/shared/_check_recovery_settings.html.haml
index 7ac90e5af03..2ba0cca9ef6 100644
--- a/app/views/shared/_check_recovery_settings.html.haml
+++ b/app/views/shared/_check_recovery_settings.html.haml
@@ -1,6 +1,11 @@
-.gl-alert.gl-alert-warning.js-recovery-settings-callout{ role: 'alert', data: { feature_id: "account_recovery_regular_check", dismiss_endpoint: user_callouts_path, defer_links: "true" } }
- %button.js-close.gl-alert-dismiss.gl-cursor-pointer{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', css_class: 'gl-icon')
+= render 'shared/global_alert',
+ variant: :warning,
+ alert_class: 'js-recovery-settings-callout',
+ alert_data: { feature_id: 'account_recovery_regular_check', dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ close_button_data: { testid: 'close-account-recovery-regular-check-callout' } do
.gl-alert-body
- - account_link_start = '<a class="deferred-link" href="%{url}">'.html_safe % { url: profile_account_path }
- = _("Please ensure your account's %{account_link_start}recovery settings%{account_link_end} are up to date.").html_safe % { account_link_start: account_link_start, account_link_end: '</a>'.html_safe }
+ = s_('Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place.')
+ = link_to _('Learn more.'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'recovery-codes'), target: '_blank', rel: 'noopener noreferrer'
+ .gl-alert-actions
+ = link_to profile_two_factor_auth_path, class: 'deferred-link btn gl-alert-action btn-confirm btn-md gl-button' do
+ = s_('Profiles|Manage two-factor authentication')
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index e96372a29db..63468340992 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -25,7 +25,8 @@
= 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.'),
+ title: group_url_error_message,
+ maxlength: ::Namespace::URL_MAX_LENGTH,
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
%p.validation-error.gl-field-error.field-validation.hide
= _("Group path is already taken. We've suggested one that is available.")
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
deleted file mode 100644
index ae0a22fd255..00000000000
--- a/app/views/shared/_recaptcha_form.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-- resource_name = spammable.class.model_name.singular
-- humanized_resource_name = spammable.class.model_name.human.downcase
-- script = local_assigns.fetch(:script, true)
-- method = params[:action] == 'create' ? :post : :put
-- has_submit = local_assigns.fetch(:has_submit, true)
-
-= form_for resource_name, method: method, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
- .recaptcha
- - params[resource_name].each do |field, value|
- = hidden_field(resource_name, field, value: value)
- = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
- -# The reCAPTCHA response value will be returned in the 'g-recaptcha-response' field
- = recaptcha_tags script: script, callback: 'recaptchaDialogCallback', nonce: content_security_policy_nonce unless Rails.env.test?
- -# Fake the 'g-recaptcha-response' field in the test environment, so that the feature spec
- -# can get to the (mocked) SpamVerdictService check.
- = hidden_field_tag('g-recaptcha-response', 'abc123') if Rails.env.test?
-
- -# Yields a block with given extra params.
- = yield
-
- - if has_submit
- .row-content-block.footer-block
- = f.submit _("Create %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'gl-button btn btn-confirm'
diff --git a/app/views/shared/_service_ping_consent.html.haml b/app/views/shared/_service_ping_consent.html.haml
index 77597124e5c..821d92e9d7e 100644
--- a/app/views/shared/_service_ping_consent.html.haml
+++ b/app/views/shared/_service_ping_consent.html.haml
@@ -1,8 +1,8 @@
- if session[:ask_for_usage_stats_consent]
- .service-ping-consent-message.gl-alert.gl-alert-info
- = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', css_class: 'gl-icon')
+ = render 'shared/global_alert',
+ variant: :info,
+ is_contained: true,
+ alert_class: 'service-ping-consent-message' do
.gl-alert-body
- docs_link = link_to _('collect usage information'), help_page_path('user/admin_area/settings/usage_statistics.md'), class: 'gl-link'
- settings_link = link_to _('your settings'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'
@@ -11,4 +11,4 @@
- send_service_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
= link_to _("Send service data"), send_service_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-service-ping-enabled': true, class: 'js-service-ping-consent-action alert-link btn gl-button btn-info'
- = link_to _("Don't send service data"), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-service-ping-enabled': false, class: 'js-service-ping-consent-action alert-link btn gl-button btn-default gl-ml-2'
+ = link_to _("Don't send service data"), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-service-ping-enabled': false, class: 'js-service-ping-consent-action alert-link btn gl-button btn-default gl-ml-3'
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 1f08bff9858..33d6b9573d4 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -7,6 +7,7 @@
%h5
= _('Active %{type} (%{token_length})') % { type: type_plural, token_length: active_tokens.length }
+
- if personal && !personal_access_token_expiration_enforced?
%p.profile-settings-content
= _("Personal access tokens are not revoked upon expiration.")
@@ -14,6 +15,9 @@
%p.profile-settings-content
= _("To see all the user's personal access tokens you must impersonate them first.")
+- if personal
+ = render_if_exists 'profiles/personal_access_tokens/token_expiry_notification', active_tokens: active_tokens
+
- if active_tokens.present?
.table-responsive
%table.table.active-tokens
@@ -42,7 +46,7 @@
%span.token-never-used-label= _('Never')
%td
- if token.expires?
- - if token.expires_at.past? || token.expires_at.today?
+ - if token.expired? || token.expired_but_not_enforced?
%span{ class: 'text-danger has-tooltip', title: _('Token valid until revoked') }
= _('Expired')
- else
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 033ed69da41..e02c24b93f1 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -24,5 +24,5 @@
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" } }
+ %button.gl-button.btn.btn-default-tertiary.btn-icon.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
= sprite_icon("maximize")
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 9ccd5655fb0..a49c17e9265 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -4,7 +4,8 @@
- @no_container = true
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
- @content_class = "issue-boards-content js-focus-mode-board"
-- if board.to_type == "EpicBoard"
+- is_epic_board = board.to_type == "EpicBoard"
+- if is_epic_board
- breadcrumb_title _("Epic Boards")
- else
- breadcrumb_title _("Issue Boards")
@@ -19,5 +20,6 @@
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
- = render "shared/boards/components/sidebar", group: group
+ - if !is_epic_board && !Feature.enabled?(:graphql_board_lists, default_enabled: :yaml)
+ = render "shared/boards/components/sidebar", group: group
%board-settings-sidebar
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 5d351bd11fd..e7bbb351633 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -1,5 +1,7 @@
%p.profile-settings-content
- = s_("DeployTokens|Pick a name for your unique deploy token.")
+ - group_deploy_tokens_help_link_url = help_page_path('user/project/deploy_tokens/index.md')
+ - group_deploy_tokens_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_deploy_tokens_help_link_url }
+ = s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: '</a>'.html_safe }
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
= form_errors(token)
@@ -7,23 +9,26 @@
.form-group
= f.label :name, class: 'label-bold'
= f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
+ .text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
.form-group
- = f.label :expires_at, _('Expires at (optional)'), class: 'label-bold'
+ = f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
= 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.')
+ .text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
.form-group
= f.label :username, _('Username (optional)'), class: 'label-bold'
= f.text_field :username, class: 'form-control'
- .text-secondary= s_('DeployTokens|Unless you specify a username, it is set to "gitlab+deploy-token-{n}".')
+ .text-secondary
+ = html_escape(s_('DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
.form-group
- = f.label :scopes, _('Scopes [Select 1 or more]'), class: 'label-bold'
+ = f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
%fieldset.form-group.form-check
= 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.')
+ .text-secondary
+ = s_('DeployTokens|Allows read-only access to the repository.')
- if container_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
@@ -34,18 +39,18 @@
%fieldset.form-group.form-check
= f.check_box :write_registry, class: 'form-check-input'
= f.label :write_registry, 'write_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows write access to registry images.')
+ .text-secondary= s_('DeployTokens|Allows read and write access to registry images.')
- if packages_registry_enabled?(group_or_project)
%fieldset.form-group.form-check
= f.check_box :read_package_registry, class: 'form-check-input', data: { qa_selector: 'deploy_token_read_package_registry_checkbox' }
= f.label :read_package_registry, 'read_package_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows read access to the package registry.')
+ .text-secondary= s_('DeployTokens|Allows read-only access to the package registry.')
%fieldset.form-group.form-check
= f.check_box :write_package_registry, class: 'form-check-input'
= f.label :write_package_registry, 'write_package_registry', class: 'label-bold form-check-label'
- .text-secondary= s_('DeployTokens|Allows write access to the package registry.')
+ .text-secondary= s_('DeployTokens|Allows read and write access to the package registry.')
.gl-mt-3
= 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 3e8368b7b78..860fb5614af 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -11,7 +11,7 @@
- if @new_deploy_token.persisted?
= render 'shared/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token
%h5.gl-mt-0
- = s_('DeployTokens|Add a deploy token')
+ = s_('DeployTokens|New deploy token')
= render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
%hr
= render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
diff --git a/app/views/shared/doorkeeper/applications/_show.html.haml b/app/views/shared/doorkeeper/applications/_show.html.haml
index b690aa74ff0..8d6b9604c1c 100644
--- a/app/views/shared/doorkeeper/applications/_show.html.haml
+++ b/app/views/shared/doorkeeper/applications/_show.html.haml
@@ -1,3 +1,5 @@
+- show_trusted_row = local_assigns.fetch(:show_trusted_row, false)
+
.table-holder.oauth-application-show
%table.table
%tr
@@ -13,11 +15,7 @@
%td
= _('Secret')
%td
- .clipboard-group
- .input-group
- %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
- .input-group-append
- = clipboard_button(target: '#secret', title: _("Copy secret"), class: "gl-button btn btn-default")
+ = clipboard_button(clipboard_text: @application.secret, button_text: _('Copy'), title: _("Copy secret"), class: "btn btn-default btn-md gl-button")
%tr
%td
= _('Callback URL')
@@ -26,6 +24,13 @@
%div
%span.monospace= uri
+ - if show_trusted_row
+ %tr
+ %td
+ = _('Trusted')
+ %td
+ = @application.trusted? ? _('Yes') : _('No')
+
%tr
%td
= _('Confidential')
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
index 506954c53ca..aaba9697fea 100644
--- a/app/views/shared/groups/_empty_state.html.haml
+++ b/app/views/shared/groups/_empty_state.html.haml
@@ -6,8 +6,3 @@
%h4= s_("GroupsEmptyState|A group is a collection of several projects.")
%p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
%p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
- - if invite_group_members?(@group)
- = link_to _('Invite your team'),
- group_group_members_path(@group),
- class: 'gl-button btn btn-confirm-secondary',
- data: { track_event: 'click_invite_team_group_empty_state', track_label: 'invite_team_group_empty_state' }
diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml
index 62f8d986296..35f302a28a6 100644
--- a/app/views/shared/integrations/_form.html.haml
+++ b/app/views/shared/integrations/_form.html.haml
@@ -1,7 +1,4 @@
- integration = local_assigns.fetch(:integration)
-%h3.page-title
- = integration.title
-
= form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration) } } do |form|
= render 'shared/service_settings', form: form, integration: integration
diff --git a/app/views/shared/integrations/_tabs.html.haml b/app/views/shared/integrations/_tabs.html.haml
new file mode 100644
index 00000000000..553401e47bd
--- /dev/null
+++ b/app/views/shared/integrations/_tabs.html.haml
@@ -0,0 +1,18 @@
+- active_tab = local_assigns.fetch(:active_tab, 'edit')
+- active_classes = 'gl-tab-nav-item-active gl-tab-nav-item-active-indigo active'
+- tabs = integration_tabs(integration: integration)
+
+- if tabs.length <= 1
+ = yield
+- else
+ .tabs.gl-tabs
+ %div
+ %ul.nav.gl-tabs-nav{ role: 'tablist' }
+ - tabs.each do |tab|
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.gl-tab-nav-item{ role: 'tab', class: (active_classes if tab[:key] == active_tab), href: tab[:href] }
+ = tab[:text]
+
+ .tab-content.gl-tab-content
+ .tab-pane.gl-pt-3.active{ role: 'tabpanel' }
+ = yield
diff --git a/app/views/shared/integrations/edit.html.haml b/app/views/shared/integrations/edit.html.haml
index a996f72e2f4..02cb94e3555 100644
--- a/app/views/shared/integrations/edit.html.haml
+++ b/app/views/shared/integrations/edit.html.haml
@@ -3,4 +3,8 @@
- page_title @integration.title, _('Integrations')
- @content_class = 'limit-container-width' unless fluid_layout
-= render 'shared/integrations/form', integration: @integration
+%h3.page-title
+ = @integration.title
+
+= render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do
+ = render 'shared/integrations/form', integration: @integration
diff --git a/app/views/shared/integrations/overrides.html.haml b/app/views/shared/integrations/overrides.html.haml
new file mode 100644
index 00000000000..dc87fae704c
--- /dev/null
+++ b/app/views/shared/integrations/overrides.html.haml
@@ -0,0 +1,10 @@
+- add_to_breadcrumbs _('Integrations'), scoped_integrations_path
+- breadcrumb_title @integration.title
+- page_title @integration.title, _('Integrations')
+- @content_class = 'limit-container-width' unless fluid_layout
+
+%h3.page-title
+ = @integration.title
+
+= render 'shared/integrations/tabs', integration: @integration, active_tab: 'overrides' do
+ .js-vue-integration-overrides{ data: integration_overrides_data(@integration) }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 6aa80e6808d..dc93442d6cd 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -31,8 +31,6 @@
= render 'shared/issuable/form/metadata', issuable: issuable, form: form, project: project, presenter: presenter
-= render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form
-
= render 'shared/issuable/form/merge_params', issuable: issuable, project: project
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 7416fda6b44..9a0b25f4015 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
#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? } }
+ directly_invite_members: can_admin_project_member?(@project) } }
.title.hide-collapsed
= _('Assignee')
= loading_icon(css_class: 'gl-vertical-align-text-bottom')
diff --git a/app/views/shared/issuable/_sidebar_user_dropdown.html.haml b/app/views/shared/issuable/_sidebar_user_dropdown.html.haml
index 3a17db5acf8..84d2fc033c8 100644
--- a/app/views/shared/issuable/_sidebar_user_dropdown.html.haml
+++ b/app/views/shared/issuable/_sidebar_user_dropdown.html.haml
@@ -1,7 +1,7 @@
- options = local_assigns.fetch(:options)
- data = options[:data]
-- if directly_invite_members?
+- if can_admin_project_member?(@project)
- options[:dropdown_class] += ' dropdown-extended-height'
- options[:footer_content] = true
- options[:wrapper_class] = local_assigns.fetch(:wrapper_class)
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index caf271e9ee9..f5bf010e4db 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -19,6 +19,7 @@
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
= sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
= sortable_item(sort_title_merged_date, page_filter_path(sort: sort_value_merged_date), sort_title) if viewing_merge_requests
+ = sortable_item(sort_title_closed_date, page_filter_path(sort: sort_value_closed_date), sort_title) if viewing_merge_requests
= sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
index a0df007f8ca..fad13c78e26 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
@@ -8,5 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[reviewer_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.reviewers), options: reviewers_dropdown_options(issuable.to_ability_name, issuable.iid, issuable.target_branch))
- - if Feature.enabled?(:mr_collapsed_approval_rules, @project)
- = render_if_exists 'shared/issuable/approver_suggestion', issuable: issuable, presenter: presenter
+ = render_if_exists 'shared/issuable/approver_suggestion', issuable: issuable, presenter: presenter
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index b5c5e2fa091..3b4ab22ce32 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -16,14 +16,14 @@
= _("Select type")
%button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') }
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
- .dropdown-content
+ .dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
%ul
%li.js-filter-issuable-type
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
- = _("Issue")
+ #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
%li.js-filter-issuable-type{ data: { track: { event: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
- = _("Incident")
+ #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")}
#js-type-popover
diff --git a/app/views/shared/nav/_sidebar_menu.html.haml b/app/views/shared/nav/_sidebar_menu.html.haml
index 9a04139d2f2..903d2d077ba 100644
--- a/app/views/shared/nav/_sidebar_menu.html.haml
+++ b/app/views/shared/nav/_sidebar_menu.html.haml
@@ -1,27 +1,30 @@
= 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?
- %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
- = sprite_icon(sidebar_menu.sprite_icon, **sidebar_menu.sprite_icon_html_options)
+ - if sidebar_menu.menu_with_partial?
+ = render_if_exists sidebar_menu.menu_partial, **sidebar_menu.menu_partial_options
+ - else
+ = 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?
+ %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
+ = sprite_icon(sidebar_menu.sprite_icon, **sidebar_menu.sprite_icon_html_options)
- %span.nav-item-name{ **sidebar_menu.title_html_options }
- = sidebar_menu.title
- - if sidebar_menu.has_pill?
- %span.badge.badge-pill.count{ **sidebar_menu.pill_html_options }
- = number_with_delimiter(sidebar_menu.pill_count)
+ %span.nav-item-name{ **sidebar_menu.title_html_options }
+ = sidebar_menu.title
+ - if sidebar_menu.has_pill?
+ %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_renderable_items?) }
- = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
- %span.fly-out-top-item-container
- %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)
+ %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
+ %span.fly-out-top-item-container
+ %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?
- %li.divider.fly-out-top-item
- = render partial: 'shared/nav/sidebar_menu_item', collection: sidebar_menu.renderable_items
+ - if sidebar_menu.has_renderable_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/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 1129fed9c3b..d0a2d97df0f 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,6 +1,6 @@
- noteable_name = @note.noteable.human_class_name
-.float-left.btn-group.gl-mr-3.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
+.float-left.btn-group.gl-sm-mr-3.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } }
- if @note.can_be_discussion_note?
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index 6f54c54d0a9..98008fede90 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -35,7 +35,7 @@
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.error-alert
- .note-form-actions.clearfix
+ .note-form-actions.clearfix.gl-display-flex.gl-flex-wrap
= render partial: 'shared/notes/comment_button'
%a.btn.gl-button.btn-cancel.js-close-discussion-note-form.hide{ role: "button", data: { cancel_text: _("Cancel") } }
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 7466f360f67..2136d287f53 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -65,6 +65,10 @@
.description.d-none.d-sm-block.gl-mr-3
= markdown_field(project, :description)
+ - if project.topics.any?
+ .gl-mt-2
+ = render "shared/projects/topics", project: project.present(current_user: current_user)
+
= render_if_exists 'shared/projects/removed', project: project
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }
diff --git a/app/views/shared/projects/_topics.html.haml b/app/views/shared/projects/_topics.html.haml
new file mode 100644
index 00000000000..a7429483da1
--- /dev/null
+++ b/app/views/shared/projects/_topics.html.haml
@@ -0,0 +1,32 @@
+- cache_enabled = false unless local_assigns[:cache_enabled] == true
+- max_project_topic_length = 15
+- project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
+
+- if project.topics.present?
+ = cache_if(cache_enabled, [project, :topic_list], expires_in: 1.day) do
+ %span.gl-w-full.gl-display-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center{ 'data-testid': 'project_topic_list' }
+ = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
+
+ - project.topics_to_show.each do |topic|
+ - explore_project_topic_path = explore_projects_path(topic: 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' }
+ = truncate(topic, length: max_project_topic_length)
+ - else
+ %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic
+
+ - if project.has_extra_topics?
+ - title = _('More topics')
+ - content = capture do
+ %span.gl-display-inline-flex.gl-flex-wrap
+ - project.topics_not_shown.each do |topic|
+ - explore_project_topic_path = explore_projects_path(topic: topic)
+ - if topic.length > max_project_topic_length
+ %a{ class: "#{ project_topics_classes } gl-mb-3 str-truncated has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
+ = truncate(topic, length: max_project_topic_length)
+ - else
+ %a{ class: "#{ project_topics_classes } gl-mb-3", href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic
+ .text-nowrap{ role: 'button', tabindex: 0, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
+ = _("+ %{count} more") % { count: project.count_of_extra_topics_not_shown }
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 4fdb9e70742..ca52a1f8f46 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -12,7 +12,7 @@
- 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} }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index effd58ad200..363909c54e2 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -62,6 +62,10 @@
- if @user&.status && user_status_set_to_busy?(@user.status)
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
+ - if @user.pronunciation.present?
+ .gl-align-items-center
+ %p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
+
- if show_status_emoji?(@user.status)
.cover-status.gl-display-inline-flex.gl-align-items-center
= emoji_icon(@user.status.emoji, class: 'gl-mr-2')
@@ -73,6 +77,10 @@
= sprite_icon('location', css_class: 'fgray')
%span{ itemprop: 'addressLocality' }
= @user.location
+ .profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mb-1.mb-sm-0
+ = sprite_icon('clock', css_class: 'fgray')
+ %span
+ = local_time(@user.timezone)
- unless work_information(@user).blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline
= sprite_icon('work', css_class: 'fgray')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index ea7709c649f..0877b11210d 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -3,7 +3,8 @@
class AdminEmailWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 8d08beb56aa..f326ae0dec8 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -247,6 +247,15 @@
:idempotent: true
:tags:
- :exclude_from_kubernetes
+- :name: cronjob:database_drop_detached_partitions
+ :worker_name: Database::DropDetachedPartitionsWorker
+ :feature_category: :database
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:database_partition_management
:worker_name: Database::PartitionManagementWorker
:feature_category: :database
@@ -256,12 +265,21 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:environments_auto_delete_cron
+ :worker_name: Environments::AutoDeleteCronWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:environments_auto_stop_cron
:worker_name: Environments::AutoStopCronWorker
:feature_category: :continuous_delivery
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
@@ -279,7 +297,7 @@
:feature_category: :service_ping
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
@@ -381,7 +399,7 @@
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
@@ -390,7 +408,7 @@
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
@@ -903,7 +921,7 @@
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags:
@@ -913,7 +931,7 @@
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags:
@@ -1020,6 +1038,7 @@
:idempotent:
:tags:
- :exclude_from_gitlab_com
+ - :needs_own_queue
- :name: hashed_storage:hashed_storage_project_migrate
:worker_name: HashedStorage::ProjectMigrateWorker
:feature_category: :source_code_management
@@ -1030,6 +1049,7 @@
:idempotent:
:tags:
- :exclude_from_gitlab_com
+ - :needs_own_queue
- :name: hashed_storage:hashed_storage_project_rollback
:worker_name: HashedStorage::ProjectRollbackWorker
:feature_category: :source_code_management
@@ -1040,6 +1060,7 @@
:idempotent:
:tags:
- :exclude_from_gitlab_com
+ - :needs_own_queue
- :name: hashed_storage:hashed_storage_rollbacker
:worker_name: HashedStorage::RollbackerWorker
:feature_category: :source_code_management
@@ -1050,6 +1071,7 @@
:idempotent:
:tags:
- :exclude_from_gitlab_com
+ - :needs_own_queue
- :name: incident_management:clusters_applications_check_prometheus_health
:worker_name: Clusters::Applications::CheckPrometheusHealthWorker
:feature_category: :incident_management
@@ -1064,7 +1086,7 @@
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 2
:idempotent:
:tags:
@@ -1083,7 +1105,7 @@
:feature_category: :incident_management
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 2
:idempotent: true
:tags: []
@@ -1096,6 +1118,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: jira_connect:jira_connect_retry_request
+ :worker_name: JiraConnect::RetryRequestWorker
+ :feature_category: :integrations
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: jira_connect:jira_connect_sync_branch
:worker_name: JiraConnect::SyncBranchWorker
:feature_category: :integrations
@@ -2028,7 +2059,8 @@
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :tags: []
+ :tags:
+ - :needs_own_queue
- :name: emails_on_push
:worker_name: EmailsOnPushWorker
:feature_category: :source_code_management
@@ -2127,7 +2159,7 @@
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent: true
:tags:
@@ -2369,7 +2401,7 @@
:feature_category: :product_analytics
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent: true
:tags:
@@ -2426,7 +2458,7 @@
:feature_category: :pages
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :cpu
:weight: 1
:idempotent:
:tags:
@@ -2771,7 +2803,8 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :tags: []
+ :tags:
+ - :needs_own_queue
- :name: snippets_schedule_bulk_repository_shard_moves
:worker_name: Snippets::ScheduleBulkRepositoryShardMovesWorker
:feature_category: :gitaly
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 f2d4404a964..41ef75ac20a 100644
--- a/app/workers/analytics/usage_trends/count_job_trigger_worker.rb
+++ b/app/workers/analytics/usage_trends/count_job_trigger_worker.rb
@@ -6,7 +6,8 @@ module Analytics
extend ::Gitlab::Utils::Override
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
DEFAULT_DELAY = 3.minutes.freeze
diff --git a/app/workers/analytics/usage_trends/counter_job_worker.rb b/app/workers/analytics/usage_trends/counter_job_worker.rb
index f4dc497d25f..bfb8a435939 100644
--- a/app/workers/analytics/usage_trends/counter_job_worker.rb
+++ b/app/workers/analytics/usage_trends/counter_job_worker.rb
@@ -6,6 +6,8 @@ module Analytics
extend ::Gitlab::Utils::Override
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :devops_reports
diff --git a/app/workers/approve_blocked_pending_approval_users_worker.rb b/app/workers/approve_blocked_pending_approval_users_worker.rb
index ff72aaad3ce..fdf1bd99558 100644
--- a/app/workers/approve_blocked_pending_approval_users_worker.rb
+++ b/app/workers/approve_blocked_pending_approval_users_worker.rb
@@ -3,6 +3,8 @@
class ApproveBlockedPendingApprovalUsersWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/authorized_keys_worker.rb b/app/workers/authorized_keys_worker.rb
index 953f493ea2c..039fe629a61 100644
--- a/app/workers/authorized_keys_worker.rb
+++ b/app/workers/authorized_keys_worker.rb
@@ -3,6 +3,8 @@
class AuthorizedKeysWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
PERMITTED_ACTIONS = %w[add_key remove_key].freeze
diff --git a/app/workers/authorized_project_update/periodic_recalculate_worker.rb b/app/workers/authorized_project_update/periodic_recalculate_worker.rb
index 2f6a9c42c0c..7b31224d151 100644
--- a/app/workers/authorized_project_update/periodic_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/periodic_recalculate_worker.rb
@@ -4,7 +4,8 @@ module AuthorizedProjectUpdate
class PeriodicRecalculateWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 52b740b4efe..1f19168cd36 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :authentication_and_authorization
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 d887a2ce25f..d83981c4ce1 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :authentication_and_authorization
diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb
index 3f0672992ef..4d350d95e7e 100644
--- a/app/workers/authorized_project_update/project_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_worker.rb
@@ -3,6 +3,8 @@
module AuthorizedProjectUpdate
class ProjectRecalculateWorker
include ApplicationWorker
+
+ data_consistency :always
include Gitlab::ExclusiveLeaseHelpers
feature_category :authentication_and_authorization
@@ -24,7 +26,7 @@ module AuthorizedProjectUpdate
private
def lock_key(project)
- "#{self.class.name.underscore}/#{project.root_namespace.id}"
+ "#{self.class.name.underscore}/projects/#{project.id}"
end
end
end
diff --git a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
index 10f7cb20df0..ef5dcc1cb99 100644
--- a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
@@ -7,6 +7,7 @@ module AuthorizedProjectUpdate
sidekiq_options retry: 3
feature_category :authentication_and_authorization
urgency :low
+ data_consistency :always
queue_namespace :authorized_project_update
idempotent!
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index a1068117e59..46fe6c7f7ce 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -3,6 +3,8 @@
class AuthorizedProjectsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
prepend WaitableWorker
diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb
index 43377382e82..9ec3e5490c2 100644
--- a/app/workers/auto_devops/disable_worker.rb
+++ b/app/workers/auto_devops/disable_worker.rb
@@ -4,6 +4,8 @@ module AutoDevops
class DisableWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include AutoDevopsQueue
diff --git a/app/workers/auto_merge_process_worker.rb b/app/workers/auto_merge_process_worker.rb
index dda0e970834..976a12f5347 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :auto_merge
diff --git a/app/workers/background_migration_worker.rb b/app/workers/background_migration_worker.rb
index 6b1f10f75b8..ef58258d998 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :database
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 531e7e5a5fe..3f3d61a8df1 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -3,6 +3,8 @@
class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index 25a86ead76e..d7f0b752a34 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -3,6 +3,8 @@
class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
feature_category :importers
tags :exclude_from_kubernetes
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index e7fce112ee1..cc52e349130 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -4,6 +4,8 @@ module BulkImports
class EntityWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
feature_category :importers
tags :exclude_from_kubernetes
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index d3bb36d830f..d5f7215b08a 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -4,6 +4,8 @@ module BulkImports
class ExportRequestWorker
include ApplicationWorker
+ data_consistency :always
+
idempotent!
worker_has_external_dependencies!
feature_category :importers
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index d3297017714..713c6c69213 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -4,6 +4,8 @@ module BulkImports
class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
NDJSON_PIPELINE_PERFORM_DELAY = 1.minute
feature_category :importers
diff --git a/app/workers/bulk_imports/relation_export_worker.rb b/app/workers/bulk_imports/relation_export_worker.rb
index 9d9449e3a1b..416dad5b3ae 100644
--- a/app/workers/bulk_imports/relation_export_worker.rb
+++ b/app/workers/bulk_imports/relation_export_worker.rb
@@ -3,6 +3,8 @@
module BulkImports
class RelationExportWorker
include ApplicationWorker
+
+ data_consistency :always
include ExceptionBacktrace
idempotent!
diff --git a/app/workers/chaos/cpu_spin_worker.rb b/app/workers/chaos/cpu_spin_worker.rb
index f8900abc764..ff468d59806 100644
--- a/app/workers/chaos/cpu_spin_worker.rb
+++ b/app/workers/chaos/cpu_spin_worker.rb
@@ -4,6 +4,8 @@ module Chaos
class CpuSpinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ChaosQueue
diff --git a/app/workers/chaos/db_spin_worker.rb b/app/workers/chaos/db_spin_worker.rb
index 9b5d06414a9..4afe11a137f 100644
--- a/app/workers/chaos/db_spin_worker.rb
+++ b/app/workers/chaos/db_spin_worker.rb
@@ -4,6 +4,8 @@ module Chaos
class DbSpinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ChaosQueue
diff --git a/app/workers/chaos/kill_worker.rb b/app/workers/chaos/kill_worker.rb
index 4148c139d42..b2f41c513a2 100644
--- a/app/workers/chaos/kill_worker.rb
+++ b/app/workers/chaos/kill_worker.rb
@@ -3,6 +3,8 @@
module Chaos
class KillWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ChaosQueue
sidekiq_options retry: false
diff --git a/app/workers/chaos/leak_mem_worker.rb b/app/workers/chaos/leak_mem_worker.rb
index 788009962db..e040a5ef807 100644
--- a/app/workers/chaos/leak_mem_worker.rb
+++ b/app/workers/chaos/leak_mem_worker.rb
@@ -4,6 +4,8 @@ module Chaos
class LeakMemWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ChaosQueue
diff --git a/app/workers/chaos/sleep_worker.rb b/app/workers/chaos/sleep_worker.rb
index b9ff5546384..149bab5d9d3 100644
--- a/app/workers/chaos/sleep_worker.rb
+++ b/app/workers/chaos/sleep_worker.rb
@@ -4,6 +4,8 @@ module Chaos
class SleepWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ChaosQueue
diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb
index 5fab437f49f..2a2e94cc6f1 100644
--- a/app/workers/chat_notification_worker.rb
+++ b/app/workers/chat_notification_worker.rb
@@ -3,6 +3,8 @@
class ChatNotificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
TimeoutExceeded = Class.new(StandardError)
sidekiq_options retry: false
diff --git a/app/workers/ci/archive_trace_worker.rb b/app/workers/ci/archive_trace_worker.rb
index 16288faf370..503cfc07c25 100644
--- a/app/workers/ci/archive_trace_worker.rb
+++ b/app/workers/ci/archive_trace_worker.rb
@@ -4,6 +4,8 @@ module Ci
class ArchiveTraceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 5fe3adf870f..12856805243 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -4,7 +4,8 @@ module Ci
class ArchiveTracesCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :continuous_integration
diff --git a/app/workers/ci/build_finished_worker.rb b/app/workers/ci/build_finished_worker.rb
index 1d6e3b1fa3c..3bca3015988 100644
--- a/app/workers/ci/build_finished_worker.rb
+++ b/app/workers/ci/build_finished_worker.rb
@@ -4,6 +4,8 @@ module Ci
class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
@@ -31,7 +33,6 @@ module Ci
# @param [Ci::Build] build The build to process.
def process_build(build)
# We execute these in sync to reduce IO.
- build.parse_trace_sections!
build.update_coverage
Ci::BuildReportResultService.new.execute(build)
diff --git a/app/workers/ci/build_prepare_worker.rb b/app/workers/ci/build_prepare_worker.rb
index f30e9d3b885..b6afad55f41 100644
--- a/app/workers/ci/build_prepare_worker.rb
+++ b/app/workers/ci/build_prepare_worker.rb
@@ -4,6 +4,8 @@ module Ci
class BuildPrepareWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
index 570f5f28c3d..56a938c32cb 100644
--- a/app/workers/ci/build_schedule_worker.rb
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -4,6 +4,8 @@ module Ci
class BuildScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 1e0da73e08d..79881ec84fd 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -4,6 +4,8 @@ module Ci
class BuildTraceChunkFlushWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
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 b38bef3bcf8..c4d76e4a4d1 100644
--- a/app/workers/ci/daily_build_group_report_results_worker.rb
+++ b/app/workers/ci/daily_build_group_report_results_worker.rb
@@ -4,6 +4,8 @@ module Ci
class DailyBuildGroupReportResultsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
diff --git a/app/workers/ci/delete_objects_worker.rb b/app/workers/ci/delete_objects_worker.rb
index ff020a3b048..d31d248597b 100644
--- a/app/workers/ci/delete_objects_worker.rb
+++ b/app/workers/ci/delete_objects_worker.rb
@@ -4,6 +4,8 @@ module Ci
class DeleteObjectsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include LimitedCapacity::Worker
diff --git a/app/workers/ci/delete_unit_tests_worker.rb b/app/workers/ci/delete_unit_tests_worker.rb
index ddfc70c43d4..d5bb72ce80c 100644
--- a/app/workers/ci/delete_unit_tests_worker.rb
+++ b/app/workers/ci/delete_unit_tests_worker.rb
@@ -3,6 +3,8 @@
module Ci
class DeleteUnitTestsWorker
include ApplicationWorker
+
+ data_consistency :always
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/ci/drop_pipeline_worker.rb b/app/workers/ci/drop_pipeline_worker.rb
index bc158433228..f3672dba3fe 100644
--- a/app/workers/ci/drop_pipeline_worker.rb
+++ b/app/workers/ci/drop_pipeline_worker.rb
@@ -4,6 +4,8 @@ module Ci
class DropPipelineWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/ci/initial_pipeline_process_worker.rb b/app/workers/ci/initial_pipeline_process_worker.rb
index ca41a7fb577..8d7a62e5b09 100644
--- a/app/workers/ci/initial_pipeline_process_worker.rb
+++ b/app/workers/ci/initial_pipeline_process_worker.rb
@@ -4,6 +4,8 @@ module Ci
class InitialPipelineProcessWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
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 bd061b5f988..af042dc1e64 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
@@ -4,6 +4,8 @@ module Ci
class AddTodoWhenBuildFailsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
index ec0cb69d0c7..16c4744eae1 100644
--- a/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb
@@ -5,6 +5,8 @@ module Ci
class CoverageReportWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
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 558153c69b2..06bc100c66a 100644
--- a/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/create_quality_report_worker.rb
@@ -5,6 +5,8 @@ module Ci
class CreateQualityReportWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :pipeline_background
diff --git a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
index 004c1d444a2..e4dc293353b 100644
--- a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
@@ -5,7 +5,8 @@ module Ci
class ExpireArtifactsWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
index b0921f6e10b..160947fb38e 100644
--- a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
@@ -4,6 +4,8 @@ module Ci
class PipelineSuccessUnlockArtifactsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
diff --git a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
index d20c501100e..0c217644cc4 100644
--- a/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
+++ b/app/workers/ci/ref_delete_unlock_artifacts_worker.rb
@@ -4,6 +4,8 @@ module Ci
class RefDeleteUnlockArtifactsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
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 ad0ed3d16f1..98b4f4ad73a 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
@@ -8,6 +8,8 @@ module Ci
class AssignResourceFromResourceGroupWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/ci/schedule_delete_objects_cron_worker.rb b/app/workers/ci/schedule_delete_objects_cron_worker.rb
index 6489665fafd..06bf83ae0a7 100644
--- a/app/workers/ci/schedule_delete_objects_cron_worker.rb
+++ b/app/workers/ci/schedule_delete_objects_cron_worker.rb
@@ -4,7 +4,8 @@ module Ci
class ScheduleDeleteObjectsCronWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
diff --git a/app/workers/ci/test_failure_history_worker.rb b/app/workers/ci/test_failure_history_worker.rb
index 3937f720788..b67797edf0b 100644
--- a/app/workers/ci/test_failure_history_worker.rb
+++ b/app/workers/ci/test_failure_history_worker.rb
@@ -4,6 +4,8 @@ module Ci
class TestFailureHistoryWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineBackgroundQueue
diff --git a/app/workers/ci_platform_metrics_update_cron_worker.rb b/app/workers/ci_platform_metrics_update_cron_worker.rb
index 05af0a0a73b..8de6e8a1253 100644
--- a/app/workers/ci_platform_metrics_update_cron_worker.rb
+++ b/app/workers/ci_platform_metrics_update_cron_worker.rb
@@ -3,7 +3,7 @@
class CiPlatformMetricsUpdateCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
# 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 a8de8efbce6..9adc026ced2 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :container_repository
diff --git a/app/workers/cluster_configure_istio_worker.rb b/app/workers/cluster_configure_istio_worker.rb
index 07c032da838..0def66b7381 100644
--- a/app/workers/cluster_configure_istio_worker.rb
+++ b/app/workers/cluster_configure_istio_worker.rb
@@ -3,6 +3,8 @@
class ClusterConfigureIstioWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ClusterQueue
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index 71374de19f5..e16e6e9ca71 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -3,6 +3,8 @@
class ClusterInstallAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 674a565f7f7..bb16cf7a5e6 100644
--- a/app/workers/cluster_patch_app_worker.rb
+++ b/app/workers/cluster_patch_app_worker.rb
@@ -3,6 +3,8 @@
class ClusterPatchAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 142ad84f746..04c9174347f 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -3,6 +3,8 @@
class ClusterProvisionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ClusterQueue
diff --git a/app/workers/cluster_update_app_worker.rb b/app/workers/cluster_update_app_worker.rb
index dc57a1a90d9..cd2b2e38ea4 100644
--- a/app/workers/cluster_update_app_worker.rb
+++ b/app/workers/cluster_update_app_worker.rb
@@ -5,6 +5,8 @@ class ClusterUpdateAppWorker # rubocop:disable Scalability/IdempotentWorker
UpdateAlreadyInProgressError = Class.new(StandardError)
include ApplicationWorker
+
+ data_consistency :always
include ClusterQueue
include ClusterApplications
include ExclusiveLeaseGuard
diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb
index 909ada2044f..bbe0cb7f0c2 100644
--- a/app/workers/cluster_upgrade_app_worker.rb
+++ b/app/workers/cluster_upgrade_app_worker.rb
@@ -3,6 +3,8 @@
class ClusterUpgradeAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 19e33cd17b0..846e4442233 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -3,6 +3,8 @@
class ClusterWaitForAppInstallationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 185959884a1..e96e03ae249 100644
--- a/app/workers/cluster_wait_for_app_update_worker.rb
+++ b/app/workers/cluster_wait_for_app_update_worker.rb
@@ -3,6 +3,8 @@
class ClusterWaitForAppUpdateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 4a010c749a2..561e72562e9 100644
--- a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -3,6 +3,8 @@
class ClusterWaitForIngressIpAddressWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 a7073b78a81..55e224887f4 100644
--- a/app/workers/clusters/applications/activate_service_worker.rb
+++ b/app/workers/clusters/applications/activate_service_worker.rb
@@ -5,6 +5,8 @@ module Clusters
class ActivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ClusterQueue
diff --git a/app/workers/clusters/applications/check_prometheus_health_worker.rb b/app/workers/clusters/applications/check_prometheus_health_worker.rb
index 4db7314cbc0..4f85c5c5b7a 100644
--- a/app/workers/clusters/applications/check_prometheus_health_worker.rb
+++ b/app/workers/clusters/applications/check_prometheus_health_worker.rb
@@ -5,7 +5,8 @@ module Clusters
class CheckPrometheusHealthWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 9337af56623..4c8d21a7c4d 100644
--- a/app/workers/clusters/applications/deactivate_service_worker.rb
+++ b/app/workers/clusters/applications/deactivate_service_worker.rb
@@ -5,6 +5,8 @@ module Clusters
class DeactivateServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ClusterQueue
@@ -14,9 +16,11 @@ module Clusters
cluster = Clusters::Cluster.find_by_id(cluster_id)
raise cluster_missing_error(integration_name) unless cluster
- integration = ::Project.integration_association_name(integration_name).to_sym
- cluster.all_projects.with_integration(integration).find_each do |project|
- project.public_send(integration).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
+ integration_class = Integration.integration_name_to_model(integration_name)
+ integration_association_name = ::Project.integration_association_name(integration_name).to_sym
+
+ cluster.all_projects.with_integration(integration_class).include_integration(integration_association_name).find_each do |project|
+ project.public_send(integration_association_name).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
index 3a4564ca7ab..da290eaf1f6 100644
--- a/app/workers/clusters/applications/uninstall_worker.rb
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -5,6 +5,8 @@ module Clusters
class UninstallWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 18801ad7e64..510ea8e7b17 100644
--- a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
+++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
@@ -5,6 +5,8 @@ module Clusters
class WaitForUninstallAppWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 e158ae0c298..6cc6c30c5e9 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -54,6 +54,10 @@ module ApplicationWorker
subclass.after_set_class_attribute { subclass.set_queue }
end
+ def generated_queue_name
+ Gitlab::SidekiqConfig::WorkerRouter.queue_name_from_worker_name(self)
+ end
+
override :validate_worker_attributes!
def validate_worker_attributes!
super
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 1eff53cea01..a377b7a2000 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -17,10 +17,6 @@ module Gitlab
feature_category :importers
worker_has_external_dependencies!
-
- def logger
- @logger ||= Gitlab::Import::Logger.build
- end
end
# project - An instance of `Project` to import the data into.
@@ -39,8 +35,24 @@ module Gitlab
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
info(project.id, message: 'importer finished')
+ rescue KeyError => e
+ # This exception will be more useful in development when a new
+ # Representation is created but the developer forgot to add a
+ # `:github_id` field.
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: importer_class.name,
+ exception: e,
+ fail_import: true
+ )
+
+ raise(e)
rescue StandardError => e
- error(project.id, e, hash)
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: importer_class.name,
+ exception: e
+ )
end
def object_type
@@ -63,28 +75,11 @@ module Gitlab
attr_accessor :github_id
def info(project_id, extra = {})
- logger.info(log_attributes(project_id, extra))
- end
-
- def error(project_id, exception, data = {})
- logger.error(
- log_attributes(
- project_id,
- message: 'importer failed',
- 'error.message': exception.message,
- 'github.data': data
- )
- )
-
- Gitlab::ErrorTracking.track_and_raise_exception(
- exception,
- log_attributes(project_id)
- )
+ Logger.info(log_attributes(project_id, extra))
end
def log_attributes(project_id, extra = {})
extra.merge(
- import_source: :github,
project_id: project_id,
importer: importer_class.name,
github_id: github_id
diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb
index 05eb7fbc2cb..e7156ac12f8 100644
--- a/app/workers/concerns/gitlab/github_import/queue.rb
+++ b/app/workers/concerns/gitlab/github_import/queue.rb
@@ -17,13 +17,10 @@ module Gitlab
sidekiq_options dead: false, retry: 5
sidekiq_retries_exhausted do |msg, e|
- Gitlab::Import::Logger.error(
- event: :github_importer_exhausted,
- message: msg['error_message'],
- class: msg['class'],
- args: msg['args'],
- exception_message: e.message,
- exception_backtrace: e.backtrace
+ Gitlab::Import::ImportFailureService.track(
+ project_id: msg['args'][0],
+ exception: e,
+ fail_import: true
)
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 916b273a28f..d7b4578af63 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -15,7 +15,14 @@ module Gitlab
info(project_id, message: 'stage finished')
rescue StandardError => e
- error(project_id, e)
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project_id,
+ exception: e,
+ error_source: self.class.name,
+ fail_import: abort_on_failure
+ )
+
+ raise(e)
end
# client - An instance of Gitlab::GithubImport::Client.
@@ -34,38 +41,22 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- private
-
- def info(project_id, extra = {})
- logger.info(log_attributes(project_id, extra))
+ def abort_on_failure
+ false
end
- def error(project_id, exception)
- logger.error(
- log_attributes(
- project_id,
- message: 'stage failed',
- 'error.message': exception.message
- )
- )
+ private
- Gitlab::ErrorTracking.track_and_raise_exception(
- exception,
- log_attributes(project_id)
- )
+ def info(project_id, extra = {})
+ Gitlab::GithubImport::Logger.info(log_attributes(project_id, extra))
end
def log_attributes(project_id, extra = {})
extra.merge(
- import_source: :github,
project_id: project_id,
import_stage: self.class.name
)
end
-
- def logger
- @logger ||= Gitlab::Import::Logger.build
- end
end
end
end
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 33dda6a8f0c..de9bb4d5a93 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -4,6 +4,8 @@ module ContainerExpirationPolicies
class CleanupContainerRepositoryWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include LimitedCapacity::Worker
include Gitlab::Utils::StrongMemoize
@@ -61,16 +63,23 @@ module ContainerExpirationPolicies
def container_repository
strong_memoize(:container_repository) do
ContainerRepository.transaction do
- # We need a lock to prevent two workers from picking up the same row
- container_repository = next_container_repository
+ repository = next_container_repository
+
+ repository&.tap do |repo|
+ log_info(
+ project_id: repo.project_id,
+ container_repository_id: repo.id
+ )
- container_repository&.tap(&:cleanup_ongoing!)
+ repo.cleanup_ongoing!
+ end
end
end
end
def next_container_repository
# rubocop: disable CodeReuse/ActiveRecord
+ # We need a lock to prevent two workers from picking up the same row
next_one_requiring = ContainerRepository.requiring_cleanup
.order(:expiration_policy_cleanup_status, :expiration_policy_started_at)
.limit(1)
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index a35ca5d184e..a791fe5d350 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -3,7 +3,8 @@
class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
include ExclusiveLeaseGuard
diff --git a/app/workers/create_commit_signature_worker.rb b/app/workers/create_commit_signature_worker.rb
index 0ba2cc41e99..01a2e109967 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
index 0af203fc3bd..4bea4fc872e 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index a9072e1661f..eb02fe72294 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -3,6 +3,8 @@
class CreatePipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb
index 5a326a351e8..0750ff1acaf 100644
--- a/app/workers/database/batched_background_migration_worker.rb
+++ b/app/workers/database/batched_background_migration_worker.rb
@@ -4,7 +4,8 @@ module Database
class BatchedBackgroundMigrationWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :database
diff --git a/app/workers/database/drop_detached_partitions_worker.rb b/app/workers/database/drop_detached_partitions_worker.rb
new file mode 100644
index 00000000000..f9c8ce57a36
--- /dev/null
+++ b/app/workers/database/drop_detached_partitions_worker.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Database
+ class DropDetachedPartitionsWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ feature_category :database
+ data_consistency :always
+ idempotent!
+
+ def perform
+ Gitlab::Database::Partitioning::DetachedPartitionDropper.new.perform
+ ensure
+ Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
+ end
+ end
+end
diff --git a/app/workers/database/partition_management_worker.rb b/app/workers/database/partition_management_worker.rb
index c9b1cd6d261..a203c76558a 100644
--- a/app/workers/database/partition_management_worker.rb
+++ b/app/workers/database/partition_management_worker.rb
@@ -4,9 +4,10 @@ module Database
class PartitionManagementWorker
include ApplicationWorker
- sidekiq_options retry: 3
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+ data_consistency :always
+
feature_category :database
idempotent!
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index f39f8bf44a4..beeca559060 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -3,6 +3,8 @@
class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExclusiveLeaseGuard
diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb
index 46dac5d8d39..54d8fcb6dfd 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
index c7e1a4da965..5e1103dbc5d 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/delete_stored_files_worker.rb b/app/workers/delete_stored_files_worker.rb
index 75113b4787c..d1080c8df64 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category_not_owned!
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index f1b9f859ce6..0af084caf86 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :authentication_and_authorization
diff --git a/app/workers/deployments/drop_older_deployments_worker.rb b/app/workers/deployments/drop_older_deployments_worker.rb
index 6ca819e7942..979f683cfb3 100644
--- a/app/workers/deployments/drop_older_deployments_worker.rb
+++ b/app/workers/deployments/drop_older_deployments_worker.rb
@@ -4,6 +4,8 @@ module Deployments
class DropOlderDeploymentsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
index 3de06c381cd..25121656408 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/deployments/forward_deployment_worker.rb b/app/workers/deployments/forward_deployment_worker.rb
index 946945051ba..7f5eb13b88d 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/deployments/hooks_worker.rb b/app/workers/deployments/hooks_worker.rb
index beac44881fb..d23a440ed36 100644
--- a/app/workers/deployments/hooks_worker.rb
+++ b/app/workers/deployments/hooks_worker.rb
@@ -4,6 +4,8 @@ module Deployments
class HooksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :delayed, feature_flag: :load_balancing_for_deployments_hooks_worker
+
queue_namespace :deployment
feature_category :continuous_delivery
diff --git a/app/workers/deployments/link_merge_request_worker.rb b/app/workers/deployments/link_merge_request_worker.rb
index 70947b3f731..81eeefcb248 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index eab331433e8..401c2d7600c 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/deployments/update_environment_worker.rb b/app/workers/deployments/update_environment_worker.rb
index 5c71a13064e..2cd9eec754c 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :deployment
diff --git a/app/workers/design_management/copy_design_collection_worker.rb b/app/workers/design_management/copy_design_collection_worker.rb
index 28b511c7c27..a498eed173c 100644
--- a/app/workers/design_management/copy_design_collection_worker.rb
+++ b/app/workers/design_management/copy_design_collection_worker.rb
@@ -4,6 +4,8 @@ module DesignManagement
class CopyDesignCollectionWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :design_management
diff --git a/app/workers/design_management/new_version_worker.rb b/app/workers/design_management/new_version_worker.rb
index eee96858c34..c3de64b841b 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :design_management
diff --git a/app/workers/destroy_pages_deployments_worker.rb b/app/workers/destroy_pages_deployments_worker.rb
index edd446628aa..36424f7473e 100644
--- a/app/workers/destroy_pages_deployments_worker.rb
+++ b/app/workers/destroy_pages_deployments_worker.rb
@@ -3,6 +3,8 @@
class DestroyPagesDeploymentsWorker
include ApplicationWorker
+ data_consistency :always
+
idempotent!
loggable_arguments 0, 1
diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb
index ef66287a692..8c7ef6c9e32 100644
--- a/app/workers/detect_repository_languages_worker.rb
+++ b/app/workers/detect_repository_languages_worker.rb
@@ -2,6 +2,8 @@
class DetectRepositoryLanguagesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ExceptionBacktrace
include ExclusiveLeaseGuard
diff --git a/app/workers/disallow_two_factor_for_group_worker.rb b/app/workers/disallow_two_factor_for_group_worker.rb
index 3a48e3ab5da..4f5ef69a730 100644
--- a/app/workers/disallow_two_factor_for_group_worker.rb
+++ b/app/workers/disallow_two_factor_for_group_worker.rb
@@ -3,6 +3,8 @@
class DisallowTwoFactorForGroupWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExceptionBacktrace
diff --git a/app/workers/disallow_two_factor_for_subgroups_worker.rb b/app/workers/disallow_two_factor_for_subgroups_worker.rb
index f5b31e0bcf0..d3528b0674b 100644
--- a/app/workers/disallow_two_factor_for_subgroups_worker.rb
+++ b/app/workers/disallow_two_factor_for_subgroups_worker.rb
@@ -3,6 +3,8 @@
class DisallowTwoFactorForSubgroupsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExceptionBacktrace
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 37ed1001c9d..1514897b2e4 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -3,12 +3,17 @@
class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :issue_tracking
urgency :high
weight 2
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1087#jobs-written-to-redis-without-passing-through-the-application
+ tags :needs_own_queue
+
attr_accessor :raw
def perform(raw)
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 9c4418c5f31..0230a226567 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
+ data_consistency :always
+
sidekiq_options retry: 3
attr_reader :email, :skip_premailer
diff --git a/app/workers/environments/auto_delete_cron_worker.rb b/app/workers/environments/auto_delete_cron_worker.rb
new file mode 100644
index 00000000000..12c08f07d95
--- /dev/null
+++ b/app/workers/environments/auto_delete_cron_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Environments
+ class AutoDeleteCronWorker
+ include ApplicationWorker
+ include ::Gitlab::LoopHelpers
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ data_consistency :always
+ feature_category :continuous_delivery
+ deduplicate :until_executed, including_scheduled: true
+ idempotent!
+
+ LOOP_TIMEOUT = 45.minutes
+ LOOP_LIMIT = 1000
+ BATCH_SIZE = 100
+
+ def perform
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ destroy_in_batch
+ end
+ end
+
+ private
+
+ def destroy_in_batch
+ environments = Environment.auto_deletable(BATCH_SIZE)
+
+ return false if environments.empty?
+
+ environments.each(&:destroy)
+ end
+ end
+end
diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb
index 64028839df1..4d6453a85e7 100644
--- a/app/workers/environments/auto_stop_cron_worker.rb
+++ b/app/workers/environments/auto_stop_cron_worker.rb
@@ -4,10 +4,12 @@ module Environments
class AutoStopCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :continuous_delivery
+ worker_resource_boundary :cpu
def perform
AutoStopService.new.execute
diff --git a/app/workers/environments/canary_ingress/update_worker.rb b/app/workers/environments/canary_ingress/update_worker.rb
index ecdfc6f0581..591c88cac96 100644
--- a/app/workers/environments/canary_ingress/update_worker.rb
+++ b/app/workers/environments/canary_ingress/update_worker.rb
@@ -5,6 +5,8 @@ module Environments
class UpdateWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: false
idempotent!
worker_has_external_dependencies!
diff --git a/app/workers/error_tracking_issue_link_worker.rb b/app/workers/error_tracking_issue_link_worker.rb
index 6c5a96822a6..37a3a1e7bcb 100644
--- a/app/workers/error_tracking_issue_link_worker.rb
+++ b/app/workers/error_tracking_issue_link_worker.rb
@@ -8,6 +8,8 @@
class ErrorTrackingIssueLinkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 9fc76a2173b..4c82c114d15 100644
--- a/app/workers/experiments/record_conversion_event_worker.rb
+++ b/app/workers/experiments/record_conversion_event_worker.rb
@@ -4,6 +4,8 @@ module Experiments
class RecordConversionEventWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :users
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index a9fa94ef301..65d387f73ed 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -3,7 +3,8 @@
class ExpireBuildArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 3e6e81867bd..96378acca08 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :continuous_integration
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 074c35997f6..cd5ca25f031 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -3,6 +3,8 @@
class ExpireJobCacheWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/export_csv_worker.rb b/app/workers/export_csv_worker.rb
index a2ad0cb92fd..68feaa61cdd 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :issue_tracking
diff --git a/app/workers/file_hook_worker.rb b/app/workers/file_hook_worker.rb
index b1422cd8795..77aaf957254 100644
--- a/app/workers/file_hook_worker.rb
+++ b/app/workers/file_hook_worker.rb
@@ -3,9 +3,11 @@
class FileHookWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: false
feature_category :integrations
loggable_arguments 0
+ urgency :low
def perform(file_name, data)
success, message = Gitlab::FileHook.execute(file_name, data)
diff --git a/app/workers/flush_counter_increments_worker.rb b/app/workers/flush_counter_increments_worker.rb
index 44d30b4ba3d..bcb6a4c2bca 100644
--- a/app/workers/flush_counter_increments_worker.rb
+++ b/app/workers/flush_counter_increments_worker.rb
@@ -8,6 +8,8 @@
class FlushCounterIncrementsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category_not_owned!
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index f25296f0461..06f0ef623c2 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -9,6 +9,8 @@ module Gitlab
class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ::Gitlab::Import::AdvanceStage
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 91dab3470d9..cce179542c7 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
@@ -6,6 +6,7 @@ module Gitlab
include ObjectImporter
tags :exclude_from_kubernetes
+ worker_resource_boundary :cpu
def representation_class
Gitlab::GithubImport::Representation::PullRequest
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 de10fe40589..8796d6392df 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
@@ -6,6 +6,7 @@ module Gitlab
include ObjectImporter
tags :exclude_from_kubernetes
+ worker_resource_boundary :cpu
def representation_class
Gitlab::GithubImport::Representation::PullRequestReview
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 1c769921ab3..2b9fb26d53a 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
class RefreshImportJidWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include GithubImport::Queue
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 f909d7e2f34..006b79dbff4 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -6,6 +6,8 @@ module Gitlab
class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 7ca23ecad20..715c39caf42 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportBaseDataWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 d66698277b0..c33836e20d1 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportIssuesAndDiffNotesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 2a66a08d534..f6f5687130f 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportLfsObjectsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 873e389fca6..0160145ffe2 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -6,6 +6,8 @@ module Gitlab
class ImportNotesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 5743648680d..7d83fe288da 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportPullRequestsMergedByWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
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 532d550f190..ea3b89efd22 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportPullRequestsReviewsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
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 5755aea21ce..d76d36531d1 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
@@ -6,6 +6,8 @@ module Gitlab
class ImportPullRequestsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 e113563ce8b..227b7c304b0 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -6,6 +6,8 @@ module Gitlab
class ImportRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include GithubImport::Queue
include StageMethods
@@ -26,7 +28,7 @@ module Gitlab
info(project.id, message: "starting importer", importer: 'Importer::RepositoryImporter')
importer = Importer::RepositoryImporter.new(project, client)
- return unless importer.execute
+ importer.execute
counter.increment
@@ -39,6 +41,10 @@ module Gitlab
'The number of imported GitHub repositories'
)
end
+
+ def abort_on_failure
+ true
+ end
end
end
end
diff --git a/app/workers/gitlab/import/stuck_import_job.rb b/app/workers/gitlab/import/stuck_import_job.rb
index 57fb3baf2b5..efbea7d8133 100644
--- a/app/workers/gitlab/import/stuck_import_job.rb
+++ b/app/workers/gitlab/import/stuck_import_job.rb
@@ -5,12 +5,13 @@ module Gitlab
module StuckImportJob
extend ActiveSupport::Concern
+ StuckImportJobError = Class.new(StandardError)
+
IMPORT_JOBS_EXPIRATION = 24.hours.seconds.to_i
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
@@ -35,9 +36,9 @@ module Gitlab
end
def mark_imports_without_jid_as_failed!
- enqueued_import_states_without_jid.each do |import_state|
- import_state.mark_as_failed(error_message)
- end.size
+ enqueued_import_states_without_jid
+ .each(&method(:mark_as_failed))
+ .size
end
def mark_imports_with_jid_as_failed!
@@ -59,9 +60,20 @@ module Gitlab
job_ids: completed_import_state_jids
)
- completed_import_states.each do |import_state|
- import_state.mark_as_failed(error_message)
- end.size
+ completed_import_states
+ .each(&method(:mark_as_failed))
+ .size
+ end
+
+ def mark_as_failed(import_state)
+ raise StuckImportJobError, error_message
+ rescue StuckImportJobError => e
+ Gitlab::Import::ImportFailureService.track(
+ import_state: import_state,
+ exception: e,
+ error_source: self.class.name,
+ fail_import: true
+ )
end
def enqueued_import_states
diff --git a/app/workers/gitlab/jira_import/advance_stage_worker.rb b/app/workers/gitlab/jira_import/advance_stage_worker.rb
index 6387054d448..5fae7caf791 100644
--- a/app/workers/gitlab/jira_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/jira_import/advance_stage_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
class AdvanceStageWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 98bde2218c2..eabe7328b92 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -5,6 +5,8 @@ module Gitlab
class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include NotifyUponDeath
include Gitlab::JiraImport::QueueOptions
@@ -52,7 +54,7 @@ module Gitlab
label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i)
- Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(LabelLink.table_name, label_link_attrs) # rubocop:disable Gitlab/BulkInsert
end
def assign_issue(project_id, issue_id, assignee_ids)
@@ -60,7 +62,7 @@ module Gitlab
assignee_attrs = assignee_ids.map { |user_id| { issue_id: issue_id, user_id: user_id } }
- Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert
+ Gitlab::Database.main.bulk_insert(IssueAssignee.table_name, assignee_attrs) # rubocop:disable Gitlab/BulkInsert
end
def build_label_attrs(issue_id, label_id)
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 e327ced8c65..e0de3ee169e 100644
--- a/app/workers/gitlab/jira_import/stage/start_import_worker.rb
+++ b/app/workers/gitlab/jira_import/stage/start_import_worker.rb
@@ -6,6 +6,8 @@ module Gitlab
class StartImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ProjectStartImport
include ProjectImportOptions
diff --git a/app/workers/gitlab/phabricator_import/import_tasks_worker.rb b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
index 867a12fbac2..f650681fc2f 100644
--- a/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
+++ b/app/workers/gitlab/phabricator_import/import_tasks_worker.rb
@@ -4,6 +4,8 @@ module Gitlab
class ImportTasksWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ProjectImportOptions # This marks the project as failed after too many tries
diff --git a/app/workers/gitlab_performance_bar_stats_worker.rb b/app/workers/gitlab_performance_bar_stats_worker.rb
index 4f7fdcf96f0..4e8bcb9af7b 100644
--- a/app/workers/gitlab_performance_bar_stats_worker.rb
+++ b/app/workers/gitlab_performance_bar_stats_worker.rb
@@ -3,6 +3,9 @@
class GitlabPerformanceBarStatsWorker
include ApplicationWorker
+ data_consistency :always
+ worker_resource_boundary :cpu
+
sidekiq_options retry: 3
LEASE_KEY = 'gitlab:performance_bar_stats'
diff --git a/app/workers/gitlab_service_ping_worker.rb b/app/workers/gitlab_service_ping_worker.rb
index a27629eac0a..6cf46458b1e 100644
--- a/app/workers/gitlab_service_ping_worker.rb
+++ b/app/workers/gitlab_service_ping_worker.rb
@@ -5,10 +5,13 @@ class GitlabServicePingWorker # rubocop:disable Scalability/IdempotentWorker
LEASE_TIMEOUT = 86400
include ApplicationWorker
+
+ data_consistency :always
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
include Gitlab::ExclusiveLeaseHelpers
feature_category :service_ping
+ worker_resource_boundary :cpu
sidekiq_options retry: 3, dead: false
sidekiq_retry_in { |count| (count + 1) * 8.hours.to_i }
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index de1e9af7bae..1bcaf5a42be 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -3,6 +3,8 @@
class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include Gitlab::ShellAdapter
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 2c140c89e26..f44c109f12d 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -3,6 +3,8 @@
class GroupDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExceptionBacktrace
diff --git a/app/workers/group_export_worker.rb b/app/workers/group_export_worker.rb
index a212147d8fd..f6f9a69fb17 100644
--- a/app/workers/group_export_worker.rb
+++ b/app/workers/group_export_worker.rb
@@ -2,6 +2,8 @@
class GroupExportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ExceptionBacktrace
feature_category :importers
diff --git a/app/workers/group_import_worker.rb b/app/workers/group_import_worker.rb
index b8b596f459b..198c6274166 100644
--- a/app/workers/group_import_worker.rb
+++ b/app/workers/group_import_worker.rb
@@ -3,6 +3,8 @@
class GroupImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: false, dead: false
feature_category :importers
diff --git a/app/workers/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb
index 735d8a2447a..80e86fd7814 100644
--- a/app/workers/hashed_storage/migrator_worker.rb
+++ b/app/workers/hashed_storage/migrator_worker.rb
@@ -4,11 +4,16 @@ module HashedStorage
class MigratorWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :hashed_storage
feature_category :source_code_management
- tags :exclude_from_gitlab_com
+
+ # Gitlab::HashedStorage::Migrator#migration_pending? depends on the
+ # queue size of this worker.
+ tags :exclude_from_gitlab_com, :needs_own_queue
# @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 0659c8a6a46..edddea55356 100644
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -4,11 +4,16 @@ module HashedStorage
class ProjectMigrateWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :hashed_storage
loggable_arguments 1
- tags :exclude_from_gitlab_com
+
+ # Gitlab::HashedStorage::Migrator#migration_pending? depends on the
+ # queue size of this worker.
+ tags :exclude_from_gitlab_com, :needs_own_queue
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 a5ee8b35176..c5841dbbb28 100644
--- a/app/workers/hashed_storage/project_rollback_worker.rb
+++ b/app/workers/hashed_storage/project_rollback_worker.rb
@@ -4,11 +4,16 @@ module HashedStorage
class ProjectRollbackWorker < BaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :hashed_storage
loggable_arguments 1
- tags :exclude_from_gitlab_com
+
+ # Gitlab::HashedStorage::Migrator#rollback_pending? depends on the
+ # queue size of this worker.
+ tags :exclude_from_gitlab_com, :needs_own_queue
attr_reader :project_id
diff --git a/app/workers/hashed_storage/rollbacker_worker.rb b/app/workers/hashed_storage/rollbacker_worker.rb
index 447bdfa6220..90e48f0e37a 100644
--- a/app/workers/hashed_storage/rollbacker_worker.rb
+++ b/app/workers/hashed_storage/rollbacker_worker.rb
@@ -4,11 +4,16 @@ module HashedStorage
class RollbackerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :hashed_storage
feature_category :source_code_management
- tags :exclude_from_gitlab_com
+
+ # Gitlab::HashedStorage::Migrator#rollback_pending? depends on the
+ # queue size of this worker.
+ tags :exclude_from_gitlab_com, :needs_own_queue
# @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 6e112a47932..4cb59c424d1 100644
--- a/app/workers/import_export_project_cleanup_worker.rb
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -3,7 +3,8 @@
class ImportExportProjectCleanupWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 46b59dc398f..58e411c7b19 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
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
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 62ed902e488..31da7b0bcfe 100644
--- a/app/workers/incident_management/add_severity_system_note_worker.rb
+++ b/app/workers/incident_management/add_severity_system_note_worker.rb
@@ -4,6 +4,9 @@ module IncidentManagement
class AddSeveritySystemNoteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+ worker_resource_boundary :cpu
+
sidekiq_options retry: 3
queue_namespace :incident_management
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 413a297a024..933d8e12d25 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :incident_management
diff --git a/app/workers/incident_management/process_alert_worker_v2.rb b/app/workers/incident_management/process_alert_worker_v2.rb
index 04bf6970578..f3049560bcd 100644
--- a/app/workers/incident_management/process_alert_worker_v2.rb
+++ b/app/workers/incident_management/process_alert_worker_v2.rb
@@ -4,6 +4,9 @@ module IncidentManagement
class ProcessAlertWorkerV2 # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+ worker_resource_boundary :cpu
+
queue_namespace :incident_management
feature_category :incident_management
diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb
index 662817b5a92..c9567e102d3 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 4378da186a7..3097a9fbc03 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -6,9 +6,10 @@ require 'socket'
class IrkerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
+ urgency :low
def perform(project_id, channels, colors, push_data, settings)
# Establish connection to irker server
diff --git a/app/workers/issuable/label_links_destroy_worker.rb b/app/workers/issuable/label_links_destroy_worker.rb
index f663c410fba..f88c061bafb 100644
--- a/app/workers/issuable/label_links_destroy_worker.rb
+++ b/app/workers/issuable/label_links_destroy_worker.rb
@@ -4,6 +4,8 @@ module Issuable
class LabelLinksDestroyWorker
include ApplicationWorker
+ data_consistency :always
+
idempotent!
feature_category :issue_tracking
diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb
index 41facab6bb9..7e2c3407772 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :issue_tracking
diff --git a/app/workers/issuables/clear_groups_issue_counter_worker.rb b/app/workers/issuables/clear_groups_issue_counter_worker.rb
index a8d6fd2f870..9e62224b83d 100644
--- a/app/workers/issuables/clear_groups_issue_counter_worker.rb
+++ b/app/workers/issuables/clear_groups_issue_counter_worker.rb
@@ -4,6 +4,8 @@ module Issuables
class ClearGroupsIssueCounterWorker
include ApplicationWorker
+ data_consistency :always
+
idempotent!
urgency :low
feature_category :issue_tracking
diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb
index 9077b42d645..ad3470ae64b 100644
--- a/app/workers/issue_due_scheduler_worker.rb
+++ b/app/workers/issue_due_scheduler_worker.rb
@@ -3,7 +3,8 @@
class IssueDueSchedulerWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 8166dda135e..e0c4502ed1a 100644
--- a/app/workers/issue_placement_worker.rb
+++ b/app/workers/issue_placement_worker.rb
@@ -3,6 +3,8 @@
class IssuePlacementWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/issue_rebalancing_worker.rb b/app/workers/issue_rebalancing_worker.rb
index 66ef7dd3152..13e02c37bdb 100644
--- a/app/workers/issue_rebalancing_worker.rb
+++ b/app/workers/issue_rebalancing_worker.rb
@@ -3,6 +3,8 @@
class IssueRebalancingWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/jira_connect/forward_event_worker.rb b/app/workers/jira_connect/forward_event_worker.rb
index 877ab46cfe5..14922db5420 100644
--- a/app/workers/jira_connect/forward_event_worker.rb
+++ b/app/workers/jira_connect/forward_event_worker.rb
@@ -4,22 +4,26 @@ module JiraConnect
class ForwardEventWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
queue_namespace :jira_connect
feature_category :integrations
+ urgency :low
+
worker_has_external_dependencies!
def perform(installation_id, base_path, event_path)
installation = JiraConnectInstallation.find_by_id(installation_id)
+ instance_url = installation&.instance_url
+
+ installation.destroy if installation
- return if installation&.instance_url.nil?
+ return if instance_url.nil?
- proxy_url = installation.instance_url + event_path
- qsh = Atlassian::Jwt.create_query_string_hash(proxy_url, 'POST', installation.instance_url + base_path)
+ proxy_url = instance_url + event_path
+ qsh = Atlassian::Jwt.create_query_string_hash(proxy_url, 'POST', instance_url + base_path)
jwt = Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret)
- Gitlab::HTTP.post(proxy_url, headers: { 'Authorization' => "JWT #{jwt}" })
- ensure
- installation.destroy if installation
+ JiraConnect::RetryRequestWorker.perform_async(proxy_url, jwt)
end
end
end
diff --git a/app/workers/jira_connect/retry_request_worker.rb b/app/workers/jira_connect/retry_request_worker.rb
new file mode 100644
index 00000000000..b0f6dada639
--- /dev/null
+++ b/app/workers/jira_connect/retry_request_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class RetryRequestWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ queue_namespace :jira_connect
+ feature_category :integrations
+ urgency :low
+
+ worker_has_external_dependencies!
+
+ def perform(proxy_url, jwt, attempts = 3)
+ r = Gitlab::HTTP.post(proxy_url, headers: { 'Authorization' => "JWT #{jwt}" })
+
+ self.class.perform_in(1.hour, proxy_url, jwt, attempts - 1) if r.code >= 400 && attempts > 0
+ rescue *Gitlab::HTTP::HTTP_ERRORS
+ self.class.perform_in(1.hour, proxy_url, jwt, attempts - 1) if attempts > 0
+ end
+ end
+end
diff --git a/app/workers/jira_connect/sync_branch_worker.rb b/app/workers/jira_connect/sync_branch_worker.rb
index 2723287b77b..1b0655e4510 100644
--- a/app/workers/jira_connect/sync_branch_worker.rb
+++ b/app/workers/jira_connect/sync_branch_worker.rb
@@ -5,11 +5,11 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
loggable_arguments 1, 2
+ urgency :low
worker_has_external_dependencies!
diff --git a/app/workers/jira_connect/sync_builds_worker.rb b/app/workers/jira_connect/sync_builds_worker.rb
index 4c4daba3314..379f087bc1b 100644
--- a/app/workers/jira_connect/sync_builds_worker.rb
+++ b/app/workers/jira_connect/sync_builds_worker.rb
@@ -5,11 +5,11 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
tags :exclude_from_kubernetes
+ urgency :low
worker_has_external_dependencies!
diff --git a/app/workers/jira_connect/sync_deployments_worker.rb b/app/workers/jira_connect/sync_deployments_worker.rb
index 0dc34b5999f..3138230ced5 100644
--- a/app/workers/jira_connect/sync_deployments_worker.rb
+++ b/app/workers/jira_connect/sync_deployments_worker.rb
@@ -5,11 +5,11 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
tags :exclude_from_kubernetes
+ urgency :low
worker_has_external_dependencies!
diff --git a/app/workers/jira_connect/sync_feature_flags_worker.rb b/app/workers/jira_connect/sync_feature_flags_worker.rb
index c484cabbe6b..4de27c1b551 100644
--- a/app/workers/jira_connect/sync_feature_flags_worker.rb
+++ b/app/workers/jira_connect/sync_feature_flags_worker.rb
@@ -5,11 +5,11 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
tags :exclude_from_kubernetes
+ urgency :low
worker_has_external_dependencies!
diff --git a/app/workers/jira_connect/sync_merge_request_worker.rb b/app/workers/jira_connect/sync_merge_request_worker.rb
index bb0d24667e9..6576aa9fdf4 100644
--- a/app/workers/jira_connect/sync_merge_request_worker.rb
+++ b/app/workers/jira_connect/sync_merge_request_worker.rb
@@ -5,10 +5,10 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
+ urgency :low
worker_has_external_dependencies!
diff --git a/app/workers/jira_connect/sync_project_worker.rb b/app/workers/jira_connect/sync_project_worker.rb
index 317bace89b4..a83444e58e7 100644
--- a/app/workers/jira_connect/sync_project_worker.rb
+++ b/app/workers/jira_connect/sync_project_worker.rb
@@ -5,11 +5,11 @@ module JiraConnect
include ApplicationWorker
sidekiq_options retry: 3
-
queue_namespace :jira_connect
feature_category :integrations
data_consistency :delayed
tags :exclude_from_kubernetes
+ urgency :low
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 44f8f1e446c..4c17f8df722 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -4,6 +4,8 @@ module MailScheduler
class IssueDueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include MailSchedulerQueue
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 8645cc93511..0e6494a45d6 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -6,6 +6,8 @@ module MailScheduler
class NotificationServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include MailSchedulerQueue
diff --git a/app/workers/member_invitation_reminder_emails_worker.rb b/app/workers/member_invitation_reminder_emails_worker.rb
index bfee8ab1fab..fa6787b9063 100644
--- a/app/workers/member_invitation_reminder_emails_worker.rb
+++ b/app/workers/member_invitation_reminder_emails_worker.rb
@@ -3,7 +3,8 @@
class MemberInvitationReminderEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb
index 0ee5654eaba..915551d6e30 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
+ data_consistency :always
+
sidekiq_options retry: 3
ENTITY_TYPES = %w(Group Project).freeze
diff --git a/app/workers/merge_request_cleanup_refs_worker.rb b/app/workers/merge_request_cleanup_refs_worker.rb
index 408d070d56f..c57c6fbc28d 100644
--- a/app/workers/merge_request_cleanup_refs_worker.rb
+++ b/app/workers/merge_request_cleanup_refs_worker.rb
@@ -5,6 +5,8 @@ class MergeRequestCleanupRefsWorker
include LimitedCapacity::Worker
include Gitlab::Utils::StrongMemoize
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/merge_request_mergeability_check_worker.rb b/app/workers/merge_request_mergeability_check_worker.rb
index 13961de1f59..0e1ab505644 100644
--- a/app/workers/merge_request_mergeability_check_worker.rb
+++ b/app/workers/merge_request_mergeability_check_worker.rb
@@ -3,16 +3,22 @@
class MergeRequestMergeabilityCheckWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
idempotent!
+ def logger
+ @logger ||= Sidekiq.logger
+ end
+
def perform(merge_request_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
unless merge_request
- logger.error("Failed to find merge request with ID: #{merge_request_id}")
+ logger.error(worker: self.class.name, message: "Failed to find merge request", merge_request_id: merge_request_id)
return
end
@@ -21,6 +27,6 @@ class MergeRequestMergeabilityCheckWorker
.new(merge_request)
.execute(recheck: false, retry_lease: false)
- logger.error("Failed to check mergeability of merge request (#{merge_request_id}): #{result.message}") if result.error?
+ logger.error(worker: self.class.name, message: "Failed to check mergeability of merge request: #{result.message}", merge_request_id: merge_request_id) if result.error?
end
end
diff --git a/app/workers/merge_requests/create_pipeline_worker.rb b/app/workers/merge_requests/create_pipeline_worker.rb
index a79a92a5419..ee42a3dee08 100644
--- a/app/workers/merge_requests/create_pipeline_worker.rb
+++ b/app/workers/merge_requests/create_pipeline_worker.rb
@@ -4,6 +4,8 @@ module MergeRequests
class CreatePipelineWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index 1ce3a99b298..69bd3949e9d 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/merge_requests/handle_assignees_change_worker.rb b/app/workers/merge_requests/handle_assignees_change_worker.rb
index 4c0500cd520..7cf1be51d23 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/merge_requests/resolve_todos_worker.rb b/app/workers/merge_requests/resolve_todos_worker.rb
index 8bb88091efe..314cdac4414 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index df5a7a904fc..3fcd7a3ad7a 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -3,6 +3,8 @@
class MergeWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/metrics/dashboard/prune_old_annotations_worker.rb b/app/workers/metrics/dashboard/prune_old_annotations_worker.rb
index 5e8067a4438..5c117486da2 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
+ data_consistency :always
+
sidekiq_options retry: 3
DELETE_LIMIT = 10_000
diff --git a/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb b/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
index 6f2ff8cca13..62cf35a669f 100644
--- a/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
+++ b/app/workers/metrics/dashboard/schedule_annotations_prune_worker.rb
@@ -5,7 +5,8 @@ module Metrics
class ScheduleAnnotationsPruneWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 0fdc7825f47..645c03428a2 100644
--- a/app/workers/metrics/dashboard/sync_dashboards_worker.rb
+++ b/app/workers/metrics/dashboard/sync_dashboards_worker.rb
@@ -5,6 +5,8 @@ module Metrics
class SyncDashboardsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :metrics
diff --git a/app/workers/migrate_external_diffs_worker.rb b/app/workers/migrate_external_diffs_worker.rb
index a73a9be4f0c..566797d8b8a 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index 91cad6f2a5c..c2ed379be48 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -9,6 +9,8 @@
class NamespacelessProjectDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExceptionBacktrace
diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb
index 1f46be29553..035fa453f59 100644
--- a/app/workers/namespaces/in_product_marketing_emails_worker.rb
+++ b/app/workers/namespaces/in_product_marketing_emails_worker.rb
@@ -4,7 +4,8 @@ module Namespaces
class InProductMarketingEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
diff --git a/app/workers/namespaces/onboarding_issue_created_worker.rb b/app/workers/namespaces/onboarding_issue_created_worker.rb
index 7b8b1a43078..3cff741ecbf 100644
--- a/app/workers/namespaces/onboarding_issue_created_worker.rb
+++ b/app/workers/namespaces/onboarding_issue_created_worker.rb
@@ -4,6 +4,8 @@ module Namespaces
class OnboardingIssueCreatedWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :issue_tracking
diff --git a/app/workers/namespaces/onboarding_pipeline_created_worker.rb b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
index 128d7b6aa06..2c77fab8114 100644
--- a/app/workers/namespaces/onboarding_pipeline_created_worker.rb
+++ b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
@@ -4,6 +4,8 @@ module Namespaces
class OnboardingPipelineCreatedWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :subgroups
diff --git a/app/workers/namespaces/onboarding_progress_worker.rb b/app/workers/namespaces/onboarding_progress_worker.rb
index d4db55a9207..43d13618091 100644
--- a/app/workers/namespaces/onboarding_progress_worker.rb
+++ b/app/workers/namespaces/onboarding_progress_worker.rb
@@ -4,9 +4,12 @@ module Namespaces
class OnboardingProgressWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :product_analytics
+ worker_resource_boundary :cpu
tags :exclude_from_kubernetes
urgency :low
diff --git a/app/workers/namespaces/onboarding_user_added_worker.rb b/app/workers/namespaces/onboarding_user_added_worker.rb
index 8c85dfafa12..4d4d9c03d3e 100644
--- a/app/workers/namespaces/onboarding_user_added_worker.rb
+++ b/app/workers/namespaces/onboarding_user_added_worker.rb
@@ -4,6 +4,8 @@ module Namespaces
class OnboardingUserAddedWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :users
diff --git a/app/workers/namespaces/prune_aggregation_schedules_worker.rb b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
index 0ea27c532ae..1084edb9b50 100644
--- a/app/workers/namespaces/prune_aggregation_schedules_worker.rb
+++ b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
@@ -4,7 +4,8 @@ module Namespaces
class PruneAggregationSchedulesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 92bf2e22020..b97dbca2c1c 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :update_namespace_statistics
diff --git a/app/workers/namespaces/schedule_aggregation_worker.rb b/app/workers/namespaces/schedule_aggregation_worker.rb
index cee273688e9..7cd7f5223d6 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :update_namespace_statistics
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index a579b828354..899545fc02c 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -3,6 +3,8 @@
class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include NewIssuable
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 574c73ad3b5..9694d44e8c1 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -3,6 +3,8 @@
class NewMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include NewIssuable
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index 566bb9a9057..e54d84e382e 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :issue_tracking
diff --git a/app/workers/object_pool/create_worker.rb b/app/workers/object_pool/create_worker.rb
index 586b81fcd30..28e130ee2bc 100644
--- a/app/workers/object_pool/create_worker.rb
+++ b/app/workers/object_pool/create_worker.rb
@@ -4,6 +4,8 @@ module ObjectPool
class CreateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ObjectPoolQueue
include ExclusiveLeaseGuard
diff --git a/app/workers/object_pool/destroy_worker.rb b/app/workers/object_pool/destroy_worker.rb
index 297780b20bd..db28c5ba064 100644
--- a/app/workers/object_pool/destroy_worker.rb
+++ b/app/workers/object_pool/destroy_worker.rb
@@ -4,6 +4,8 @@ module ObjectPool
class DestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ObjectPoolQueue
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
index 282a8f54695..36094ddbe84 100644
--- a/app/workers/object_pool/join_worker.rb
+++ b/app/workers/object_pool/join_worker.rb
@@ -4,6 +4,8 @@ module ObjectPool
class JoinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ObjectPoolQueue
diff --git a/app/workers/object_pool/schedule_join_worker.rb b/app/workers/object_pool/schedule_join_worker.rb
index 44208208d04..a2f4ba63b69 100644
--- a/app/workers/object_pool/schedule_join_worker.rb
+++ b/app/workers/object_pool/schedule_join_worker.rb
@@ -4,6 +4,8 @@ module ObjectPool
class ScheduleJoinWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ObjectPoolQueue
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
index fba91e49e43..2204e504702 100644
--- a/app/workers/object_storage/background_move_worker.rb
+++ b/app/workers/object_storage/background_move_worker.rb
@@ -3,6 +3,8 @@
module ObjectStorage
class BackgroundMoveWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ObjectStorageQueue
sidekiq_options retry: 5
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 7323ab50370..ea4a90cf9d2 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -5,6 +5,8 @@ module ObjectStorage
class MigrateUploadsWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ObjectStorageQueue
diff --git a/app/workers/packages/composer/cache_cleanup_worker.rb b/app/workers/packages/composer/cache_cleanup_worker.rb
index 1d47ef87962..fee886bc750 100644
--- a/app/workers/packages/composer/cache_cleanup_worker.rb
+++ b/app/workers/packages/composer/cache_cleanup_worker.rb
@@ -5,7 +5,8 @@ module Packages
class CacheCleanupWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :package_registry
diff --git a/app/workers/packages/composer/cache_update_worker.rb b/app/workers/packages/composer/cache_update_worker.rb
index d87abf2e256..f146a0a83cc 100644
--- a/app/workers/packages/composer/cache_update_worker.rb
+++ b/app/workers/packages/composer/cache_update_worker.rb
@@ -5,6 +5,8 @@ module Packages
class CacheUpdateWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :package_registry
diff --git a/app/workers/packages/debian/generate_distribution_worker.rb b/app/workers/packages/debian/generate_distribution_worker.rb
index 68fdd80ffb1..b9b157d25d2 100644
--- a/app/workers/packages/debian/generate_distribution_worker.rb
+++ b/app/workers/packages/debian/generate_distribution_worker.rb
@@ -4,6 +4,8 @@ module Packages
module Debian
class GenerateDistributionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include Gitlab::Utils::StrongMemoize
# The worker is idempotent, by reusing component files with the same file_sha256.
diff --git a/app/workers/packages/debian/process_changes_worker.rb b/app/workers/packages/debian/process_changes_worker.rb
index edc366a7597..3ddeb858429 100644
--- a/app/workers/packages/debian/process_changes_worker.rb
+++ b/app/workers/packages/debian/process_changes_worker.rb
@@ -4,6 +4,8 @@ module Packages
module Debian
class ProcessChangesWorker
include ApplicationWorker
+
+ data_consistency :always
include Gitlab::Utils::StrongMemoize
deduplicate :until_executed
diff --git a/app/workers/packages/go/sync_packages_worker.rb b/app/workers/packages/go/sync_packages_worker.rb
index c5f631c47db..182c9bfec5d 100644
--- a/app/workers/packages/go/sync_packages_worker.rb
+++ b/app/workers/packages/go/sync_packages_worker.rb
@@ -5,6 +5,8 @@ module Packages
class SyncPackagesWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include Gitlab::Golang
diff --git a/app/workers/packages/helm/extraction_worker.rb b/app/workers/packages/helm/extraction_worker.rb
index fd4e720da94..1010a0833b1 100644
--- a/app/workers/packages/helm/extraction_worker.rb
+++ b/app/workers/packages/helm/extraction_worker.rb
@@ -5,6 +5,8 @@ module Packages
class ExtractionWorker
include ApplicationWorker
+ data_consistency :always
+
queue_namespace :package_repositories
feature_category :package_registry
deduplicate :until_executing
diff --git a/app/workers/packages/maven/metadata/sync_worker.rb b/app/workers/packages/maven/metadata/sync_worker.rb
index c53117a08c5..ab18c70e95e 100644
--- a/app/workers/packages/maven/metadata/sync_worker.rb
+++ b/app/workers/packages/maven/metadata/sync_worker.rb
@@ -6,6 +6,8 @@ module Packages
class SyncWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include Gitlab::Utils::StrongMemoize
diff --git a/app/workers/packages/nuget/extraction_worker.rb b/app/workers/packages/nuget/extraction_worker.rb
index 4128b229ebe..b8e00621aa1 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
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :package_repositories
diff --git a/app/workers/packages/rubygems/extraction_worker.rb b/app/workers/packages/rubygems/extraction_worker.rb
index fc32654a2c1..520305981cf 100644
--- a/app/workers/packages/rubygems/extraction_worker.rb
+++ b/app/workers/packages/rubygems/extraction_worker.rb
@@ -5,6 +5,8 @@ module Packages
class ExtractionWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
queue_namespace :package_repositories
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
index cc720676214..4744c855394 100644
--- a/app/workers/pages_domain_removal_cron_worker.rb
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -3,7 +3,8 @@
class PagesDomainRemovalCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :pages
diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
index c99eed8a8df..a551078c91a 100644
--- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
@@ -3,10 +3,12 @@
class PagesDomainSslRenewalCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :pages
+ worker_resource_boundary :cpu
def perform
return unless ::Gitlab::LetsEncrypt.enabled?
diff --git a/app/workers/pages_domain_ssl_renewal_worker.rb b/app/workers/pages_domain_ssl_renewal_worker.rb
index 2ab41aab795..d4c68f66699 100644
--- a/app/workers/pages_domain_ssl_renewal_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_worker.rb
@@ -3,6 +3,8 @@
class PagesDomainSslRenewalWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :pages
diff --git a/app/workers/pages_domain_verification_cron_worker.rb b/app/workers/pages_domain_verification_cron_worker.rb
index ec63004716a..56339d50a40 100644
--- a/app/workers/pages_domain_verification_cron_worker.rb
+++ b/app/workers/pages_domain_verification_cron_worker.rb
@@ -3,10 +3,12 @@
class PagesDomainVerificationCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :pages
+ worker_resource_boundary :cpu
def perform
return if Gitlab::Database.read_only?
diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb
index b67b1b4d8ee..f9504a7c1d2 100644
--- a/app/workers/pages_domain_verification_worker.rb
+++ b/app/workers/pages_domain_verification_worker.rb
@@ -3,6 +3,8 @@
class PagesDomainVerificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :pages
diff --git a/app/workers/pages_remove_worker.rb b/app/workers/pages_remove_worker.rb
index 3e60df9027a..69a8344b5aa 100644
--- a/app/workers/pages_remove_worker.rb
+++ b/app/workers/pages_remove_worker.rb
@@ -4,6 +4,8 @@
class PagesRemoveWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :pages
tags :exclude_from_kubernetes
diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb
index 0d80ec28310..c2190a352dd 100644
--- a/app/workers/pages_transfer_worker.rb
+++ b/app/workers/pages_transfer_worker.rb
@@ -3,6 +3,8 @@
class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
TransferFailedError = Class.new(StandardError)
diff --git a/app/workers/pages_update_configuration_worker.rb b/app/workers/pages_update_configuration_worker.rb
index 8bb9f76670b..8a37b70a0b7 100644
--- a/app/workers/pages_update_configuration_worker.rb
+++ b/app/workers/pages_update_configuration_worker.rb
@@ -3,6 +3,8 @@
class PagesUpdateConfigurationWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index ee394271653..d0c21cf74e1 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -3,10 +3,13 @@
class PagesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :pages
loggable_arguments 0, 1
tags :requires_disk_io, :exclude_from_kubernetes
+ worker_resource_boundary :cpu
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 bb4834ab2dd..3f74dc3812a 100644
--- a/app/workers/partition_creation_worker.rb
+++ b/app/workers/partition_creation_worker.rb
@@ -3,7 +3,8 @@
class PartitionCreationWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 73568960d38..8deacf457b2 100644
--- a/app/workers/personal_access_tokens/expired_notification_worker.rb
+++ b/app/workers/personal_access_tokens/expired_notification_worker.rb
@@ -4,7 +4,8 @@ module PersonalAccessTokens
class ExpiredNotificationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :authentication_and_authorization
diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb
index aaca78e3c63..f4afa9f8994 100644
--- a/app/workers/personal_access_tokens/expiring_worker.rb
+++ b/app/workers/personal_access_tokens/expiring_worker.rb
@@ -4,7 +4,8 @@ module PersonalAccessTokens
class ExpiringWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :authentication_and_authorization
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 40d138752b4..322f92d376b 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -12,7 +12,7 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline.includes({ builds: { runner: :tags } })
+ Ci::Pipeline
.find_by(id: pipeline_id)
.try(:execute_hooks)
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index fdab10d7dda..c2580c3d48b 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -3,6 +3,8 @@
class PipelineMetricsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index 619570dcf41..640f3494d58 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -8,6 +8,7 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high
worker_resource_boundary :cpu
+ data_consistency :delayed
def perform(pipeline_id, args = {})
case args
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index a35b32c35f2..9cd471a5ab6 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -3,6 +3,8 @@
class PipelineProcessWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index f1248ec9e58..ebda30f57d8 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,7 +3,8 @@
class PipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :continuous_integration
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index ce985492935..4a49e18eb9b 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,6 +3,8 @@
class PostReceive # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include Gitlab::Experiment::Dsl
@@ -124,7 +126,6 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
end
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
repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs)
SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 54ffe8d3b10..a4dfe11c394 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -10,6 +10,8 @@
class ProcessCommitWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index d2796cdb697..328fdc4717c 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -4,6 +4,8 @@
class ProjectCacheWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
LEASE_TIMEOUT = 15.minutes.to_i
diff --git a/app/workers/project_daily_statistics_worker.rb b/app/workers/project_daily_statistics_worker.rb
index 7d673ec00d9..02f8958f82a 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index be11fa65028..149f8290b54 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -3,6 +3,8 @@
class ProjectDestroyWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExceptionBacktrace
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 1c4aa3f7e49..4dd9a9c6fcb 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -2,6 +2,8 @@
class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ExceptionBacktrace
feature_category :importers
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index da38d2fc0cd..6d809dfb22b 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -3,10 +3,12 @@
class ProjectServiceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
sidekiq_options dead: false
feature_category :integrations
+ urgency :low
+
worker_has_external_dependencies!
def perform(hook_id, data)
diff --git a/app/workers/projects/post_creation_worker.rb b/app/workers/projects/post_creation_worker.rb
index 389e987e81a..99438e4e4b2 100644
--- a/app/workers/projects/post_creation_worker.rb
+++ b/app/workers/projects/post_creation_worker.rb
@@ -4,6 +4,8 @@ module Projects
class PostCreationWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
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 55530bff7c1..d7f207f45e8 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
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/prometheus/create_default_alerts_worker.rb b/app/workers/prometheus/create_default_alerts_worker.rb
index 9d163cd828e..94ac02c4c04 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :incident_management
diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb
index 6881740461f..443ff1f2abe 100644
--- a/app/workers/propagate_integration_group_worker.rb
+++ b/app/workers/propagate_integration_group_worker.rb
@@ -3,10 +3,12 @@
class PropagateIntegrationGroupWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
tags :exclude_from_kubernetes
+ urgency :low
+
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/propagate_integration_inherit_descendant_worker.rb b/app/workers/propagate_integration_inherit_descendant_worker.rb
index 9067af12de3..24573591409 100644
--- a/app/workers/propagate_integration_inherit_descendant_worker.rb
+++ b/app/workers/propagate_integration_inherit_descendant_worker.rb
@@ -3,10 +3,12 @@
class PropagateIntegrationInheritDescendantWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
tags :exclude_from_kubernetes
+ urgency :low
+
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/propagate_integration_inherit_worker.rb b/app/workers/propagate_integration_inherit_worker.rb
index e7649d6714f..24a8778b928 100644
--- a/app/workers/propagate_integration_inherit_worker.rb
+++ b/app/workers/propagate_integration_inherit_worker.rb
@@ -3,10 +3,12 @@
class PropagateIntegrationInheritWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
tags :exclude_from_kubernetes
+ urgency :low
+
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb
index 90cf27c4176..dba8a270007 100644
--- a/app/workers/propagate_integration_project_worker.rb
+++ b/app/workers/propagate_integration_project_worker.rb
@@ -3,10 +3,12 @@
class PropagateIntegrationProjectWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
tags :exclude_from_kubernetes
+ urgency :low
+
idempotent!
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/propagate_integration_worker.rb b/app/workers/propagate_integration_worker.rb
index 0f8229bdf09..9d21d92b6e3 100644
--- a/app/workers/propagate_integration_worker.rb
+++ b/app/workers/propagate_integration_worker.rb
@@ -3,11 +3,13 @@
class PropagateIntegrationWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
- idempotent!
loggable_arguments 1
+ urgency :low
+
+ idempotent!
def perform(integration_id)
Admin::PropagateIntegrationService.propagate(Integration.find(integration_id))
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index 149577b15cd..6b8382ae433 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
-# Worker for updating any project specific caches.
+# No longer in use https://gitlab.com/groups/gitlab-org/-/epics/5672
+# To be removed https://gitlab.com/gitlab-org/gitlab/-/issues/335178
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :integrations
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
index 59d324bc573..2d74c271ddc 100644
--- a/app/workers/prune_old_events_worker.rb
+++ b/app/workers/prune_old_events_worker.rb
@@ -3,7 +3,8 @@
class PruneOldEventsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 9f1ea8a6eb4..8ab4e77bc78 100644
--- a/app/workers/purge_dependency_proxy_cache_worker.rb
+++ b/app/workers/purge_dependency_proxy_cache_worker.rb
@@ -3,6 +3,8 @@
class PurgeDependencyProxyCacheWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include Gitlab::Allowable
idempotent!
diff --git a/app/workers/rebase_worker.rb b/app/workers/rebase_worker.rb
index 664905eb9e5..7121ce0e205 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/releases/create_evidence_worker.rb b/app/workers/releases/create_evidence_worker.rb
index bd790e8d0ee..5aed543500f 100644
--- a/app/workers/releases/create_evidence_worker.rb
+++ b/app/workers/releases/create_evidence_worker.rb
@@ -4,6 +4,8 @@ module Releases
class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :release_evidence
diff --git a/app/workers/releases/manage_evidence_worker.rb b/app/workers/releases/manage_evidence_worker.rb
index 88b6c4aea06..f316aa6eefd 100644
--- a/app/workers/releases/manage_evidence_worker.rb
+++ b/app/workers/releases/manage_evidence_worker.rb
@@ -4,7 +4,8 @@ module Releases
class ManageEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :release_evidence
diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb
index 39a7c0fc79d..ed3a0ccb989 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index edf3a02cff5..37298c53a5c 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -3,7 +3,8 @@
class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 9940953207e..c9eb715a522 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -3,7 +3,8 @@
class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :authentication_and_authorization
diff --git a/app/workers/remove_unaccepted_member_invites_worker.rb b/app/workers/remove_unaccepted_member_invites_worker.rb
index c1f8e3881f1..7833ec30c3c 100644
--- a/app/workers/remove_unaccepted_member_invites_worker.rb
+++ b/app/workers/remove_unaccepted_member_invites_worker.rb
@@ -3,7 +3,8 @@
class RemoveUnacceptedMemberInvitesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :authentication_and_authorization
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
index ca4b70a0485..c67ab6e356a 100644
--- a/app/workers/remove_unreferenced_lfs_objects_worker.rb
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -3,7 +3,8 @@
class RemoveUnreferencedLfsObjectsWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 84cafba17cf..c713f54faa5 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -3,7 +3,8 @@
class RepositoryArchiveCacheWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 a8744638d7b..88a8f98aaf6 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,6 +4,8 @@ module RepositoryCheck
class BatchWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include RepositoryCheckQueue
include ExclusiveLeaseGuard
diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb
index bc19b42da1a..5d2db1b4472 100644
--- a/app/workers/repository_check/clear_worker.rb
+++ b/app/workers/repository_check/clear_worker.rb
@@ -4,6 +4,8 @@ module RepositoryCheck
class ClearWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include RepositoryCheckQueue
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
index 30734926765..fc41eab2c16 100644
--- a/app/workers/repository_check/dispatch_worker.rb
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -4,7 +4,8 @@ module RepositoryCheck
class DispatchWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 31d68e65b23..4c74a647464 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -4,6 +4,8 @@ module RepositoryCheck
class SingleRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include RepositoryCheckQueue
diff --git a/app/workers/repository_cleanup_worker.rb b/app/workers/repository_cleanup_worker.rb
index 03c9add6afb..2b4f41f9612 100644
--- a/app/workers/repository_cleanup_worker.rb
+++ b/app/workers/repository_cleanup_worker.rb
@@ -3,6 +3,8 @@
class RepositoryCleanupWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 06a6f5b0600..5ec9ceaf004 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -3,6 +3,8 @@
class RepositoryForkWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 0f86d55df22..413bb135943 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -2,6 +2,8 @@
class RepositoryImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+
+ data_consistency :always
include ExceptionBacktrace
include ProjectStartImport
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
index 48158cda857..c95393e7d21 100644
--- a/app/workers/repository_remove_remote_worker.rb
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -3,6 +3,8 @@
class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ExclusiveLeaseGuard
@@ -14,22 +16,13 @@ class RepositoryRemoveRemoteWorker # rubocop:disable Scalability/IdempotentWorke
attr_reader :project, :remote_name
def perform(project_id, remote_name)
- @remote_name = remote_name
- @project = Project.find_by_id(project_id)
-
- return unless @project
-
- logger.info("Removing remote #{remote_name} from project #{project.id}")
-
- try_obtain_lease do
- remove_remote = @project.repository.remove_remote(remote_name)
-
- if remove_remote
- logger.info("Remote #{remote_name} was successfully removed from project #{project.id}")
- else
- logger.error("Could not remove remote #{remote_name} from project #{project.id}")
- end
- end
+ # On-disk remotes are slated for removal, and GitLab doesn't create any of
+ # them anymore. For backwards compatibility, we need to keep the worker
+ # though such that we can be sure to drain all jobs on an update. Making
+ # this a no-op is fine though: the worst that can happen is that we still
+ # have old remotes lingering in the repository's config, but Gitaly will
+ # start to clean these up in repository maintenance.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/336745
end
def lease_timeout
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index 483aae84a3b..9265449fdf4 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -4,6 +4,8 @@ class RepositoryUpdateRemoteMirrorWorker
UpdateError = Class.new(StandardError)
include ApplicationWorker
+
+ data_consistency :always
include Gitlab::ExclusiveLeaseHelpers
worker_has_external_dependencies!
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
index 35c18177a81..e02b63fb621 100644
--- a/app/workers/requests_profiles_worker.rb
+++ b/app/workers/requests_profiles_worker.rb
@@ -3,7 +3,8 @@
class RequestsProfilesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 553153848c7..dd0f14a5cab 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -3,6 +3,8 @@
class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/schedule_merge_request_cleanup_refs_worker.rb b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
index 40a773ca58f..46a6e0ef01f 100644
--- a/app/workers/schedule_merge_request_cleanup_refs_worker.rb
+++ b/app/workers/schedule_merge_request_cleanup_refs_worker.rb
@@ -3,7 +3,8 @@
class ScheduleMergeRequestCleanupRefsWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :code_review
diff --git a/app/workers/schedule_migrate_external_diffs_worker.rb b/app/workers/schedule_migrate_external_diffs_worker.rb
index ecafe8f5e7d..b2dea5083b4 100644
--- a/app/workers/schedule_migrate_external_diffs_worker.rb
+++ b/app/workers/schedule_migrate_external_diffs_worker.rb
@@ -3,7 +3,8 @@
class ScheduleMigrateExternalDiffsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 9dc3bb855fb..97d858eddd9 100644
--- a/app/workers/self_monitoring_project_create_worker.rb
+++ b/app/workers/self_monitoring_project_create_worker.rb
@@ -3,6 +3,8 @@
class SelfMonitoringProjectCreateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 c155c57dec7..74dc4cb6581 100644
--- a/app/workers/self_monitoring_project_delete_worker.rb
+++ b/app/workers/self_monitoring_project_delete_worker.rb
@@ -3,6 +3,8 @@
class SelfMonitoringProjectDeleteWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
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 cfe63e059bb..f546fce3e8a 100644
--- a/app/workers/service_desk_email_receiver_worker.rb
+++ b/app/workers/service_desk_email_receiver_worker.rb
@@ -3,9 +3,14 @@
class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
feature_category :service_desk
sidekiq_options retry: 3
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1087#jobs-written-to-redis-without-passing-through-the-application
+ tags :needs_own_queue
+
def should_perform?
::Gitlab::ServiceDeskEmail.enabled?
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 88b060a454a..15669ac5852 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
+ data_consistency :always
+
sidekiq_options retry: 3
idempotent!
diff --git a/app/workers/ssh_keys/expired_notification_worker.rb b/app/workers/ssh_keys/expired_notification_worker.rb
index b67849942b0..6afeecdd1b5 100644
--- a/app/workers/ssh_keys/expired_notification_worker.rb
+++ b/app/workers/ssh_keys/expired_notification_worker.rb
@@ -4,7 +4,8 @@ module SshKeys
class ExpiredNotificationWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :compliance_management
diff --git a/app/workers/ssh_keys/expiring_soon_notification_worker.rb b/app/workers/ssh_keys/expiring_soon_notification_worker.rb
index d87e31c36a5..ef256621e07 100644
--- a/app/workers/ssh_keys/expiring_soon_notification_worker.rb
+++ b/app/workers/ssh_keys/expiring_soon_notification_worker.rb
@@ -4,7 +4,8 @@ module SshKeys
class ExpiringSoonNotificationWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :compliance_management
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index e206a51a417..e0d8958fc80 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -3,6 +3,8 @@
class StageUpdateWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index b3b3d6e7554..5723380a3f3 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -3,7 +3,8 @@
class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :continuous_integration
diff --git a/app/workers/stuck_export_jobs_worker.rb b/app/workers/stuck_export_jobs_worker.rb
index 398f2c915a9..486d40c443a 100644
--- a/app/workers/stuck_export_jobs_worker.rb
+++ b/app/workers/stuck_export_jobs_worker.rb
@@ -4,7 +4,8 @@
class StuckExportJobsWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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 e50b218e1f6..3a650fcb7bf 100644
--- a/app/workers/stuck_merge_jobs_worker.rb
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -3,7 +3,8 @@
class StuckMergeJobsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 8c801f2bed8..02239c6e764 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb
index 8a43ea3c2e0..91a8777fe9b 100644
--- a/app/workers/todos_destroyer/confidential_issue_worker.rb
+++ b/app/workers/todos_destroyer/confidential_issue_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class ConfidentialIssueWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/todos_destroyer/destroyed_issuable_worker.rb b/app/workers/todos_destroyer/destroyed_issuable_worker.rb
index a3a8147095e..ff4f5e15472 100644
--- a/app/workers/todos_destroyer/destroyed_issuable_worker.rb
+++ b/app/workers/todos_destroyer/destroyed_issuable_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class DestroyedIssuableWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb
index 166d8701f7a..8e3944e0a23 100644
--- a/app/workers/todos_destroyer/entity_leave_worker.rb
+++ b/app/workers/todos_destroyer/entity_leave_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class EntityLeaveWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/todos_destroyer/group_private_worker.rb b/app/workers/todos_destroyer/group_private_worker.rb
index 30d1f74fb28..76bf034037d 100644
--- a/app/workers/todos_destroyer/group_private_worker.rb
+++ b/app/workers/todos_destroyer/group_private_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class GroupPrivateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/todos_destroyer/private_features_worker.rb b/app/workers/todos_destroyer/private_features_worker.rb
index d6a4260a464..150e1c8a50e 100644
--- a/app/workers/todos_destroyer/private_features_worker.rb
+++ b/app/workers/todos_destroyer/private_features_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class PrivateFeaturesWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb
index c4fed03f11a..3d86df0c548 100644
--- a/app/workers/todos_destroyer/project_private_worker.rb
+++ b/app/workers/todos_destroyer/project_private_worker.rb
@@ -4,6 +4,8 @@ module TodosDestroyer
class ProjectPrivateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include TodosDestroyerQueue
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index 8322110b753..73af4444d60 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -3,7 +3,8 @@
class TrendingProjectsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 cf08c650d0d..d94b7ac1a72 100644
--- a/app/workers/update_container_registry_info_worker.rb
+++ b/app/workers/update_container_registry_info_worker.rb
@@ -3,7 +3,8 @@
class UpdateContainerRegistryInfoWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
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 ee47cbd6523..2a319a7f6f9 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
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 f1dd250f432..61fe278e016 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -3,6 +3,8 @@
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include PipelineQueue
diff --git a/app/workers/update_highest_role_worker.rb b/app/workers/update_highest_role_worker.rb
index cecf3f99b50..d5df46c172b 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :utilization
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 6f86a7e7e2f..421a6e47425 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :code_review
diff --git a/app/workers/update_project_statistics_worker.rb b/app/workers/update_project_statistics_worker.rb
index c93c32f4e75..4a32753ac70 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/upload_checksum_worker.rb b/app/workers/upload_checksum_worker.rb
index 765e3a63e75..8e250f38e2c 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :geo_replication
diff --git a/app/workers/user_status_cleanup/batch_worker.rb b/app/workers/user_status_cleanup/batch_worker.rb
index f46b4119f9b..b6ca6548572 100644
--- a/app/workers/user_status_cleanup/batch_worker.rb
+++ b/app/workers/user_status_cleanup/batch_worker.rb
@@ -5,7 +5,8 @@ module UserStatusCleanup
class BatchWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# rubocop:disable Scalability/CronWorkerContext
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
diff --git a/app/workers/users/create_statistics_worker.rb b/app/workers/users/create_statistics_worker.rb
index e44039f2016..0c27d165ded 100644
--- a/app/workers/users/create_statistics_worker.rb
+++ b/app/workers/users/create_statistics_worker.rb
@@ -4,7 +4,8 @@ module Users
class CreateStatisticsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
# 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
index e583823312f..bcb13483379 100644
--- a/app/workers/users/deactivate_dormant_users_worker.rb
+++ b/app/workers/users/deactivate_dormant_users_worker.rb
@@ -4,6 +4,8 @@ module Users
class DeactivateDormantUsersWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
include CronjobQueue
feature_category :utilization
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 525a72e02ef..af351c3c207 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -3,6 +3,8 @@
class WaitForClusterCreationWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ data_consistency :always
+
sidekiq_options retry: 3
include ClusterQueue
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index a2a53ca922a..5b4567dde29 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -6,16 +6,18 @@ class WebHookWorker
include ApplicationWorker
feature_category :integrations
- worker_has_external_dependencies!
loggable_arguments 2
data_consistency :delayed
-
sidekiq_options retry: 4, dead: false
+ urgency :low
+
+ worker_has_external_dependencies!
def perform(hook_id, data, hook_name)
- hook = WebHook.find(hook_id)
- data = data.with_indifferent_access
+ hook = WebHook.find_by_id(hook_id)
+ return unless hook
+ data = data.with_indifferent_access
WebHookService.new(hook, data, hook_name, jid).execute
end
end
diff --git a/app/workers/web_hooks/destroy_worker.rb b/app/workers/web_hooks/destroy_worker.rb
index c1886576c41..b92fe86bafb 100644
--- a/app/workers/web_hooks/destroy_worker.rb
+++ b/app/workers/web_hooks/destroy_worker.rb
@@ -4,11 +4,12 @@ module WebHooks
class DestroyWorker
include ApplicationWorker
+ data_consistency :always
sidekiq_options retry: 3
-
feature_category :integrations
tags :exclude_from_kubernetes
urgency :low
+
idempotent!
def perform(user_id, web_hook_id)
diff --git a/app/workers/web_hooks/log_execution_worker.rb b/app/workers/web_hooks/log_execution_worker.rb
index 58059370200..50d91182c80 100644
--- a/app/workers/web_hooks/log_execution_worker.rb
+++ b/app/workers/web_hooks/log_execution_worker.rb
@@ -4,10 +4,12 @@ module WebHooks
class LogExecutionWorker
include ApplicationWorker
- idempotent!
+ data_consistency :always
feature_category :integrations
urgency :low
+ idempotent!
+
# This worker accepts an extra argument. This enables us to
# treat this worker as idempotent. Currently this is set to
# the Job ID (jid) of the parent worker.
diff --git a/app/workers/x509_certificate_revoke_worker.rb b/app/workers/x509_certificate_revoke_worker.rb
index cbf9fbb7525..3e170c9df22 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
+ data_consistency :always
+
sidekiq_options retry: 3
feature_category :source_code_management
diff --git a/app/workers/x509_issuer_crl_check_worker.rb b/app/workers/x509_issuer_crl_check_worker.rb
index d66ad6c1c15..39440504927 100644
--- a/app/workers/x509_issuer_crl_check_worker.rb
+++ b/app/workers/x509_issuer_crl_check_worker.rb
@@ -3,7 +3,8 @@
class X509IssuerCrlCheckWorker
include ApplicationWorker
- sidekiq_options retry: 3
+ data_consistency :always
+
include CronjobQueue
feature_category :source_code_management